Java tutorial
/* * Copyright 2013 Google Inc. All Rights Reserved. * * Licensed 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 com.alow.servlet; import static com.alow.dao.ofy.OfyService.ofy; import java.io.IOException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.alow.exception.DaoException; import com.alow.exception.EntityExistsException; import com.alow.model.Pessoa; import com.alow.model.plus.DirectedUserToUserEdge; import com.alow.model.plus.Jsonifiable; import com.alow.model.plus.User; import com.alow.service.PessoaService; import com.alow.service.impl.PessoaServiceImpl; import com.google.api.client.auth.oauth2.TokenResponseException; import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest; import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; import com.google.api.services.oauth2.Oauth2; import com.google.api.services.oauth2.model.Tokeninfo; import com.google.api.services.plus.Plus; import com.google.api.services.plus.model.PeopleFeed; import com.google.api.services.plus.model.Person; import com.google.gson.annotations.Expose; /** * Provides an API to connect users to Photohunt. This servlet provides the * /api/connect end-point, and exposes the following operations: * * POST /api/connect * * @author silvano@google.com (Silvano Luciani) */ public class ConnectServlet extends JsonRestServlet { /** * Exposed as `POST /api/connect`. * * Takes the following payload in the request body. The payload represents * all of the parameters that are required to authorize and connect the user * to the app. * { * "state":"", * "access_token":"", * "token_type":"", * "expires_in":"", * "code":"", * "id_token":"", * "authuser":"", * "session_state":"", * "prompt":"", * "client_id":"", * "scope":"", * "g_user_cookie_policy":"", * "cookie_policy":"", * "issued_at":"", * "expires_at":"", * "g-oauth-window":"" * } * * Returns the following JSON response representing the User that was * connected: * { * "id":0, * "googleUserId":"", * "googleDisplayName":"", * "googlePublicProfileUrl":"", * "googlePublicProfilePhotoUrl":"", * "googleExpiresAt":0 * } * * Issues the following errors along with corresponding HTTP response codes: * 401: The error from the Google token verification end point. * 500: "Failed to upgrade the authorization code." This can happen during * OAuth v2 code exchange flows. * 500: "Failed to read token data from Google." * This response also sends the error from the token verification * response concatenated to the error message. * 500: "Failed to query the Google+ API: " * This error also includes the error from the client library * concatenated to the error response. * 500: "IOException occurred." The IOException could happen when any * IO-related errors occur such as network connectivity loss or local * file-related errors. * * @see javax.servlet.http.HttpServlet#doPost( * javax.servlet.http.HttpServletRequest, * javax.servlet.http.HttpServletResponse) */ @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { TokenData accessToken = null; try { // read the token accessToken = Jsonifiable.fromJson(req.getReader(), TokenData.class); } catch (IOException e) { sendError(resp, 400, "Unable to read auth result from request body"); } // Create a credential object. GoogleCredential credential = new GoogleCredential.Builder().setJsonFactory(JSON_FACTORY) .setTransport(TRANSPORT).setClientSecrets(CLIENT_ID, CLIENT_SECRET).build(); try { if (accessToken.code != null) { // exchange the code for a token (Web Frontend) GoogleTokenResponse tokenFromExchange = exchangeCode(accessToken); credential.setFromTokenResponse(tokenFromExchange); } else { if (accessToken.access_token == null) { sendError(resp, 400, "Missing access token in request."); } // use the token received from the client credential.setAccessToken(accessToken.access_token).setRefreshToken(accessToken.refresh_token) .setExpiresInSeconds(accessToken.expires_in) .setExpirationTimeMilliseconds(accessToken.expires_at); } // ensure that we consider logged in the user that owns the access token String tokenGoogleUserId = verifyToken(credential); User user = saveTokenForUser(tokenGoogleUserId, credential); // save the user in the session HttpSession session = req.getSession(); session.setAttribute(CURRENT_USER_SESSION_KEY, user.id); generateFriends(user, credential); sendResponse(req, resp, user); } catch (TokenVerificationException e) { sendError(resp, 401, e.getMessage()); } catch (TokenResponseException e) { sendError(resp, 500, "Failed to upgrade the authorization code."); } catch (TokenDataException e) { sendError(resp, 500, "Failed to read token data from Google. " + e.getMessage()); } catch (IOException e) { sendError(resp, 500, e.getMessage()); } catch (GoogleApiException e) { sendError(resp, 500, "Failed to query the Google+ API: " + e.getMessage()); } catch (DaoException e) { sendError(resp, 500, "Failed to access database: " + e.getMessage()); } catch (EntityExistsException e) { sendError(resp, 500, "Entity already exists: " + e.getMessage()); } } /** * Exchanges the `code` member of the given AccessToken object, and returns * the relevant GoogleTokenResponse. * @param accessToken Container of authorization code to exchange. * @return Token response from Google indicating token information. * @throws TokenDataException Failed to exchange code (code invalid). */ private GoogleTokenResponse exchangeCode(TokenData accessToken) throws TokenDataException { try { // Upgrade the authorization code into an access and refresh token. GoogleTokenResponse tokenResponse = new GoogleAuthorizationCodeTokenRequest(TRANSPORT, JSON_FACTORY, CLIENT_ID, CLIENT_SECRET, accessToken.code, "postmessage").execute(); return tokenResponse; } catch (IOException e) { throw new TokenDataException(e.getMessage()); } } /** * Verify that the token in the given credential is valid. * @param credential Credential to verify. * @return Google user ID for which token was issued. * @throws TokenVerificationException Credential is not valid. * @throws IOException Could not verify Credential because of a network * failure. */ private String verifyToken(GoogleCredential credential) throws TokenVerificationException, IOException { // Check that the token is valid. Oauth2 oauth2 = new Oauth2.Builder(TRANSPORT, JSON_FACTORY, credential).build(); Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(credential.getAccessToken()).execute(); // If there was an error in the token info, abort. if (tokenInfo.containsKey("error")) { throw new TokenVerificationException(tokenInfo.get("error").toString()); } if (credential.getExpiresInSeconds() == null) { // Set the expiry time if it hasn't already been set. int expiresIn = tokenInfo.getExpiresIn(); credential.setExpiresInSeconds((long) expiresIn); credential.setExpirationTimeMilliseconds(System.currentTimeMillis() + expiresIn * 1000); } Pattern p = Pattern.compile("(\\d+)([-]?)(.*)$"); Matcher issuedTo = p.matcher(CLIENT_ID); Matcher localId = p.matcher(tokenInfo.getIssuedTo()); // Make sure the token we got is for our app. if (!issuedTo.matches() || !localId.matches() || !issuedTo.group(1).equals(localId.group(1))) { throw new TokenVerificationException("Token's client ID does not match app's."); } return tokenInfo.getUserId(); } /** * Either: * 1. Create a user for the given ID and credential * 2. or, update the existing user with the existing credential * * If 2, then ask Google for the user's public profile information to store. * * @param tokenGoogleUserId Google user ID to update. * @param credential Credential to set for the user. * @return Updated User. * @throws GoogleApiException Could not fetch profile info for user. * @throws EntityExistsException * @throws DaoException */ private User saveTokenForUser(String tokenGoogleUserId, GoogleCredential credential) throws GoogleApiException, DaoException, EntityExistsException { User user = ofy().load().type(User.class).filter("googleUserId", tokenGoogleUserId).first().get(); if (user == null) { // Register a new user. Collect their Google profile info first. Plus plus = new Plus.Builder(TRANSPORT, JSON_FACTORY, credential).build(); Person profile; Plus.People.Get get; try { get = plus.people().get("me"); profile = get.execute(); } catch (IOException e) { throw new GoogleApiException(e.getMessage()); } user = new User(); user.setGoogleUserId(profile.getId()); user.setGoogleDisplayName(profile.getDisplayName()); user.setGooglePublicProfileUrl(profile.getUrl()); user.setGooglePublicProfilePhotoUrl(profile.getImage().getUrl()); } // TODO(silvano): Also fetch and set the email address for the user. user.setGoogleAccessToken(credential.getAccessToken()); if (credential.getRefreshToken() != null) { user.setGoogleRefreshToken(credential.getRefreshToken()); } user.setGoogleExpiresAt(credential.getExpirationTimeMilliseconds()); user.setGoogleExpiresIn(credential.getExpiresInSeconds()); ofy().save().entity(user).now(); PessoaService service = new PessoaServiceImpl(); service.generateOrUpdatePessoa(user); return user; } public void generateOrUpdatePessoa(User user) throws DaoException, EntityExistsException { System.out.println(">>>> Entrou no generate !!!!!!!!"); PessoaService service = new PessoaServiceImpl(); Pessoa pessoa = service.getPessoaByGoogleUserId(user.googleUserId); if (pessoa == null) { pessoa = new Pessoa(user.googleUserId, user.googleDisplayName, user.googlePublicProfileUrl, user.googlePublicProfilePhotoUrl); } else { pessoa.setGoogleDisplayName(user.googleDisplayName); pessoa.setGooglePublicProfileUrl(user.googlePublicProfileUrl); pessoa.setGooglePublicProfilePhotoUrl(user.googlePublicProfilePhotoUrl); } service.getCrud().save(pessoa); } /** * Query Google for the list of the user's friends that they've shared with * our app, and then store those friends for later use. * @param user User for which to get friends. * @param credential Credential to use to authorize people.list request. * @throws IOException Unable to fetch friends because of network error. */ private void generateFriends(User user, GoogleCredential credential) throws IOException { // TODO(silvano): Refactor this method to not have race condition. // TODO(silvano): Refactor this method to use task queue. // TODO(silvano): Refactor this method to fetch more than first page. // Simple but inefficient way of building the friends list Plus plus = new Plus.Builder(TRANSPORT, JSON_FACTORY, credential).build(); Plus.People.List get; List<DirectedUserToUserEdge> friends = ofy().load().type(DirectedUserToUserEdge.class) .filter("ownerUserId", user.getId()).list(); ofy().delete().entities(friends); get = plus.people().list(user.getGoogleUserId(), "visible"); PeopleFeed feed = get.execute(); boolean done = false; do { for (Person googlePlusPerson : feed.getItems()) { User friend = ofy().load().type(User.class).filter("googleUserId", googlePlusPerson.getId()).first() .get(); if (friend != null) { DirectedUserToUserEdge friendEdge = new DirectedUserToUserEdge(); friendEdge.setOwnerUserId(user.getId()); friendEdge.setFriendUserId(friend.getId()); ofy().save().entity(friendEdge).now(); } } done = true; } while (!done); } /** * Thrown when token data can't be read from verification end-point. */ class TokenDataException extends Exception { public TokenDataException(String message) { super(message); } } /** * Exception thrown when errors occurs during token verification. */ class TokenVerificationException extends Exception { public TokenVerificationException(String message) { super(message); } } /** * Exception thrown when errors occurs querying the Google + API. */ class GoogleApiException extends Exception { public GoogleApiException(String message) { super(message); } } /** * Simple Jsonifiable to represent token information sent/retrieved from our * app and its clients (web, Android, iOS). */ public static class TokenData extends Jsonifiable { public static String kind = "photohunt#tokendata"; /** * Google access token used to authorize requests to Google. */ @Expose public String access_token; /** * Google refresh token used to get new access tokens when needed. */ @Expose public String refresh_token; /** * Authorization code used to exchange for an access/refresh token pair. */ @Expose public String code; /** * Identity token for this user. */ @Expose public String id_token; /** * When the access token expires. */ @Expose public Long expires_at; /** * How long until the access token expires. */ @Expose public Long expires_in; } }