Reputation: 2597
I am accessing connection.fetchUserProfile()
from Connection<?> connection
but it gives org.springframework.social.UncategorizedApiException: (#100) Tried accessing nonexisting field (context) on node type (User)
. This particular error never happen before until now.
maven :
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-facebook</artifactId>
<version>3.0.0.M3</version>
</dependency>
Any idea why is this happening?
Upvotes: 3
Views: 6422
Reputation: 69
EDIT#2: This issue came up again for me. If you want to figure out how to modify some package and add it to maven, fine go ahead and read on, but if you're just here to figure out how to get rid of that error (and other similar errors like this involving different properties) then you should just follow the advice in this answer: https://stackoverflow.com/a/34133450/754988. It is much better than mine.
While attacomsian's solution may work for some, I depend on an abstract UserProfile for other services like Google sign in and their solution would break that for me.
A less invasive maneuver that I've found works is to head over to ss-facebook's github and follow the first two lines of their instructions to build from source:
git clone git://github.com/SpringSource/spring-social-facebook.git
cd spring-social-facebook
But before you run the ./gradlew build
command, open up this file for editing:
./spring-social-facebook/src/main/java/org/springframework/social/facebook/api/UserOperations.java
Scroll down to the bottom and remove "context" from the array, changing
static final String[] PROFILE_FIELDS = {
"id", "about", "age_range", "birthday", "context", "cover", "currency", "devices", "education", "email",
....
"website", "work"
};
to
static final String[] PROFILE_FIELDS = {
"id", "about", "age_range", "birthday", "cover", "currency", "devices", "education", "email",
....
"website", "work"
};
Now go back to the source folder and run the final command:
./gradlew build
You'll get a ./build
folder there with a zip file in it, unzip it to find your working jars! I use maven, and replaced the two remote dependencies that I had with some local dependencies, like so:
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-facebook</artifactId>
<version>3.0.0.M1</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/spring-social-facebook-3.0.0.BUILD-SNAPSHOT.jar</systemPath>
</dependency>
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-facebook-web</artifactId>
<version>3.0.0.M1</version>
<scope>system</scope>
<systemPath>${project.basedir}/src/main/resources/lib/spring-social-facebook-web-3.0.0.BUILD-SNAPSHOT.jar</systemPath>
</dependency>
EDIT: I have since learned this is not the optimal way to include a custom jar like this with Maven. It's a bit of a rabbit hole to actually get it in there where it works like other jars in your pom. I got it to work better eventually by running these commands to manually install the two jars I made (3.0.1337 is the version I made up to avoid name collision - not sure if it's necessary) ...
mvn org.apache.maven.plugins:maven-install-plugin:2.3.1:install-file -Dfile=web/src/main/resources/lib/spring-social-facebook-3.0.1337.BUILD-SNAPSHOT.jar -DgroupId=org.springframework.social -DartifactId=spring-social-facebook -Dversion=3.0.1337 -Dpackaging=jar -DlocalRepositoryPath=web/src/main/resources/local-repo -DgeneratePom=true
mvn org.apache.maven.plugins:maven-install-plugin:2.3.1:install-file -Dfile=web/src/main/resources/lib/spring-social-facebook-web-3.0.1337.BUILD-SNAPSHOT.jar -DgroupId=org.springframework.social -DartifactId=spring-social-facebook-web -Dversion=3.0.1337 -Dpackaging=jar -DlocalRepositoryPath=web/src/main/resources/local-repo -DgeneratePom=true
...Into a local repository that I declared in my pom like so:
<repositories>
<repository>
<id>local-repo</id>
<url>file://${project.basedir}/src/main/resources/local-repo</url>
</repository>
</repositories>
and then I added the jars to source control and added a line in my gitignore to ignore the /src/main/resources/local-repo folder.
If anyone has a better way of doing this that retains all of the original code, only removing that "context" string from the array at the end of that file, and hopefully avoids ALL of this junk in favor of a simple singular class replacement or extension, please chime in. I'm just including all of it because it definitely would have saved me a day of having to figure each of these things out one by one.
DISCLAIMER: I do not know what side effects this may have and I am otherwise unfamiliar with this library. Although I doubt that this will hurt much and is clearly an improvement in this one area, the change may have ramifications of which I could have no reasonable foresight without putting more time in working on this library. As such I am not supplying a modified JAR, only writing down some easy instructions for everyone else. Proceed with these instructions at your own risk.
Also, I just checked the license and if you do make this change make sure you follow the instructions that say:
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
If I'm missing any other legal stuff, please comment to let me know.
Upvotes: 0
Reputation: 579
I solved it by overriding the missing field with any other one. Add this somewhere in your code:
// Patch: spring social facebook tries to get a removed "context" field
for (int i=0; i<UserOperations.PROFILE_FIELDS.length; i++) {
// To avoid error override with existing "ratings" field
if (UserOperations.PROFILE_FIELDS[i].equals("context")) {
UserOperations.PROFILE_FIELDS[i] = "ratings";
}
}
Upvotes: 0
Reputation: 2913
I had the same issue and looked for a solution everywhere. Without any luck, I ended up writing a custom service that calls the Facebook Graph API and populates the UserProfile
object.
Add a new FBService.java
class to your project:
package net.attacomsian.services;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.*;
import org.springframework.social.connect.UserProfile;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
@Service
public class FBService {
private final RestTemplate restTemplate;
public FBService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
public UserProfile getProfile(String id, String accessToken) {
try {
//params
String params = "fields=id,name,email,first_name,last_name&access_token=" + accessToken;
//build url
String url = "https://graph.facebook.com/v3.2/" + id + "?" + params;
//create headers
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
// create request
HttpEntity request = new HttpEntity(headers);
//use rest template
ResponseEntity<String> response = this.restTemplate.exchange(url, HttpMethod.GET, request, String.class);
//check for status code
if (response.getStatusCode().is2xxSuccessful()) {
JsonNode root = new ObjectMapper().readTree(response.getBody());
// return a user profile object
return new UserProfile(root.path("id").asText(), root.path("name").asText(), root.path("first_name").asText(),
root.path("last_name").asText(), root.path("email").asText(), null);
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
}
v3.2
is the Graph API version. Replace it with your own. Now inject this service to your controller and instead of calling:
UserProfile profile = connection.fetchUserProfile();
Call the new service getProfile()
method like below:
Profile profile = fbService.getProfile("me", connection.createData().getAccessToken());
It works like a magic for me.
Update: Here is the complete web controller code that shows how you can use the custom service for Facebook while keep using the default one for Google's user profile:
@Controller
public class AuthController {
private FBService fbService;
private final ProviderSignInUtils providerSignInUtils;
public AuthController(FBService fbService,
ConnectionFactoryLocator connectionFactoryLocator,
UsersConnectionRepository connectionRepository) {
this.fbService = fbService;
this.providerSignInUtils = new ProviderSignInUtils(connectionFactoryLocator, connectionRepository);
}
@GetMapping("/social-signup")
public String socialSignup(WebRequest request, HttpSession session) {
Connection<?> connection = providerSignInUtils.getConnectionFromSession(request);
if (connection == null) {
return "redirect:/login";
}
//fetch user information
UserProfile profile = null;
if (connection.getKey().getProviderId().equalsIgnoreCase("google")) {
profile = connection.fetchUserProfile();
} else if (connection.getKey().getProviderId().equalsIgnoreCase("facebook")) {
profile = fbService.getProfile("me", connection.createData().getAccessToken());
}
// TODO: continue doing everything else
}
}
Upvotes: 4
Reputation: 420
It looks like facebook social will be not updated anymore.
Anyway, I don't want to spend time for switching to another lib.
I've just fetched the fields I need:
FacebookTemplate facebook = new FacebookTemplate(socialToken);
User facebookUser = facebook.fetchObject(FacebookUtils.LOGGED_USER_ID, User.class, FacebookUtils.USER_FIELD_ID, FacebookUtils.USER_FIELD_EMAIL,
FacebookUtils.USER_FIELD_FIRST_NAME, FacebookUtils.USER_FIELD_LAST_NAME);
Fields I've added into my FacebookUtils:
/** The Constant LOGGED_USER_ID. */
public static final String LOGGED_USER_ID = "me";
/** The Constant USER_FIELD_ID. */
public static final String USER_FIELD_ID = "id";
/** The Constant USER_FIELD_EMAIL. */
public static final String USER_FIELD_EMAIL = "email";
/** The Constant USER_FIELD_FIRST_NAME. */
public static final String USER_FIELD_FIRST_NAME = "first_name";
/** The Constant USER_FIELD_LAST_NAME. */
public static final String USER_FIELD_LAST_NAME = "last_name";
Upvotes: 4
Reputation: 566
In answer to @JohnTD, if you are using multiple social connections then you can just do the following to incorporate @attacomsian answer
public void getProfile(Connection connection) {
UserProfile socialMediaProfile = getSocialUserProfile(connection);
.
.
}
/**
* Checks for facebook connection requests so that we can call a custom service
*
* @param connection
* @return
*/
private UserProfile getSocialUserProfile(Connection<?> connection) {
if (isFacebookConnection(connection)) {
return facebookService.getProfile("me", connection.createData().getAccessToken());
} else {
return connection.fetchUserProfile();
}
}
private boolean isFacebookConnection(Connection<?> connection) {
return connection.getApi().toString().indexOf("FacebookTemplate") != -1;
}
Upvotes: 0