package org.fluxtream.connectors.moves;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.fluxtream.core.Configuration;
import org.fluxtream.core.auth.AuthHelper;
import org.fluxtream.core.connectors.Connector;
import org.fluxtream.connectors.controllers.ControllerSupport;
import org.fluxtream.core.connectors.updaters.UpdateFailedException;
import org.fluxtream.core.domain.ApiKey;
import org.fluxtream.core.domain.Guest;
import org.fluxtream.core.domain.Notification;
import org.fluxtream.core.domain.metadata.FoursquareVenue;
import org.fluxtream.core.services.GuestService;
import org.fluxtream.core.services.JPADaoService;
import org.fluxtream.core.services.MetadataService;
import org.fluxtream.core.services.NotificationsService;
import org.fluxtream.core.utils.HttpUtils;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* User: candide
* Date: 17/06/13
* Time: 16:49
*/
@Controller
@RequestMapping(value = "/moves")
public class MovesController {
@Autowired
Configuration env;
@Autowired
NotificationsService notificationsService;
@Autowired
MetadataService metadataService;
@Autowired
GuestService guestService;
@Autowired
JPADaoService jpaDaoService;
static final Logger logger = Logger.getLogger(MovesController.class);
@RequestMapping(value = "/oauth2/token")
public String getToken(HttpServletRequest request) throws IOException, ServletException {
String redirectUri = getRedirectUri();
// Check that the redirectUri is going to work
final String validRedirectUrl = env.get("moves.validRedirectURL");
if (!validRedirectUrl.startsWith(ControllerSupport.getLocationBase(request, env))) {
final long guestId = AuthHelper.getGuestId();
final String validRedirectBase = getBaseURL(validRedirectUrl);
notificationsService.addNamedNotification(guestId, Notification.Type.WARNING, Connector.getConnector("moves").statusNotificationName(),
"Adding a Moves connector only works when logged in through " + validRedirectBase +
". You are logged in through " + ControllerSupport.getLocationBase(request, env) +
".<br>Please re-login via the supported URL or inform your Fluxtream administrator " +
"that the moves.validRedirectURL setting does not match your needs.");
return "redirect:/app";
}
// Here we know that the redirectUri will work
String approvalPageUrl = String.format("https://api.moves-app.com/oauth/v1/authorize?" +
"redirect_uri=%s&" +
"response_type=code&client_id=%s&" +
"scope=activity location",
redirectUri, env.get("moves.client.id"));
final String apiKeyIdParameter = request.getParameter("apiKeyId");
if (apiKeyIdParameter !=null && !StringUtils.isEmpty(apiKeyIdParameter))
approvalPageUrl += "&state=" + apiKeyIdParameter;
return "redirect:" + approvalPageUrl;
}
public static String getBaseURL(String url) {
try {
URI uri = new URI(url);
StringBuilder rootURI = new StringBuilder(uri.getScheme()).append("://").append(uri.getHost());
if(uri.getPort()!=-1) {
rootURI.append(":" + uri.getPort());
}
return (rootURI.toString());
}
catch (URISyntaxException e) {
return null;
}
}
private String getRedirectUri() {
// TODO: This should be checked against the moves.validRedirectURL property to make
// sure that it will work. Moves only accepts the specific redirect URI's which matches the one
// configured for this key.
return env.get("homeBaseUrl") + "moves/oauth2/swapToken";
}
@RequestMapping(value = "/oauth2/swapToken")
public String swapToken(HttpServletRequest request) throws Exception {
final String errorMessage = request.getParameter("error");
final Guest guest = AuthHelper.getGuest();
Connector connector = Connector.getConnector("moves");
if (errorMessage!=null) {
notificationsService.addNamedNotification(guest.getId(),
Notification.Type.ERROR, connector.statusNotificationName(),
"There was an error while setting you up with the moves service: " + errorMessage);
return "redirect:/app";
}
final String code = request.getParameter("code");
Map<String,String> parameters = new HashMap<String,String>();
parameters.put("grant_type", "authorization_code");
parameters.put("code", code);
parameters.put("client_id", env.get("moves.client.id"));
parameters.put("client_secret", env.get("moves.client.secret"));
parameters.put("redirect_uri", getRedirectUri());
final String json = HttpUtils.fetch("https://api.moves-app.com/oauth/v1/access_token", parameters);
JSONObject token = JSONObject.fromObject(json);
if (token.has("error")) {
String errorCode = token.getString("error");
notificationsService.addNamedNotification(guest.getId(),
Notification.Type.ERROR,
connector.statusNotificationName(),
errorCode);
// NOTE: In the future if we implement renew for the Moves connector
// we will potentially need to mark the connector as permanently failed.
// The way to do this is to get hold of the existing apiKey and do:
// guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_PERMANENT_FAILURE, null);
return "redirect:/app";
}
final String refresh_token = token.getString("refresh_token");
// Create the entry for this new apiKey in the apiKey table and populate
// ApiKeyAttributes with all of the keys fro oauth.properties needed for
// subsequent update of this connector instance.
ApiKey apiKey;
final String stateParameter = request.getParameter("state");
if (stateParameter !=null&&!StringUtils.isEmpty(stateParameter)) {
long apiKeyId = Long.valueOf(stateParameter);
apiKey = guestService.getApiKey(apiKeyId);
} else {
apiKey = guestService.createApiKey(guest.getId(), Connector.getConnector("moves"));
}
guestService.populateApiKey(apiKey.getId());
guestService.setApiKeyAttribute(apiKey,
"accessToken", token.getString("access_token"));
guestService.setApiKeyAttribute(apiKey,
"tokenExpires", String.valueOf(System.currentTimeMillis() + (token.getLong("expires_in")*1000)));
guestService.setApiKeyAttribute(apiKey,
"refreshToken", refresh_token);
// Record that this connector is now up
guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_UP, null, null);
if (stateParameter !=null&&!StringUtils.isEmpty(stateParameter))
return "redirect:/app/tokenRenewed/moves";
else
return "redirect:/app/from/moves";
}
String getAccessToken(final ApiKey apiKey) throws Exception {
final String expiresString = guestService.getApiKeyAttribute(apiKey, "tokenExpires");
long expires = Long.valueOf(expiresString);
if (expires<System.currentTimeMillis())
refreshToken(apiKey);
return guestService.getApiKeyAttribute(apiKey, "accessToken");
}
private void refreshToken(final ApiKey apiKey) throws Exception, UpdateFailedException {
// Check to see if we are running on a mirrored test instance
// and should therefore refrain from swapping tokens lest we
// invalidate an existing token instance
String disableTokenSwap = env.get("disableTokenSwap");
Connector connector = Connector.getConnector("moves");
if(disableTokenSwap!=null && disableTokenSwap.equals("true")) {
String msg = "**** Skipping refreshToken for moves connector instance because disableTokenSwap is set on this server";
;
StringBuilder sb2 = new StringBuilder("module=MovesController component=MovesController action=refreshToken apiKeyId=" + apiKey.getId())
.append(" message=\"").append(msg).append("\"");
logger.info(sb2.toString());
System.out.println(msg);
// Notify the user that the tokens need to be manually renewed
notificationsService.addNamedNotification(apiKey.getGuestId(), Notification.Type.WARNING, connector.statusNotificationName(),
"Heads Up. This server cannot automatically refresh your Moves authentication tokens.<br>" +
"Please head to <a href=\"javascript:App.manageConnectors()\">Manage Connectors</a>,<br>" +
"scroll to the Moves connector, delete the connector, and re-add<br>" +
"<p>We apologize for the inconvenience</p>");
// Record permanent failure since this connector won't work again until
// it is reauthenticated
guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_PERMANENT_FAILURE, null, ApiKey.PermanentFailReason.NEEDS_REAUTH);
throw new UpdateFailedException("requires token reauthorization", true, ApiKey.PermanentFailReason.NEEDS_REAUTH);
}
// We're not on a mirrored test server. Try to swap the expired
// access token for a fresh one. Typically moves access tokens are good for
// 180 days from time of issue.
String swapTokenUrl = "https://api.moves-app.com/oauth/v1/access_token";
final String refreshToken = guestService.getApiKeyAttribute(apiKey, "refreshToken");
Map<String,String> params = new HashMap<String,String>();
params.put("refresh_token", refreshToken);
params.put("client_id", guestService.getApiKeyAttribute(apiKey, "moves.client.id"));
params.put("client_secret", guestService.getApiKeyAttribute(apiKey, "moves.client.secret"));
params.put("grant_type", "refresh_token");
String fetched;
try {
fetched = HttpUtils.fetch(swapTokenUrl, params);
// Record that this connector is now up
guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_UP, null, null);
} catch (Exception e) {
// Notify the user that the tokens need to be manually renewed
notificationsService.addNamedNotification(apiKey.getGuestId(), Notification.Type.WARNING, connector.statusNotificationName(),
"Heads Up. We failed in our attempt to automatically refresh your Moves authentication tokens.<br>" +
"Please head to <a href=\"javascript:App.manageConnectors()\">Manage Connectors</a>,<br>" +
"scroll to the Moves connector, delete the connector, and re-add<br>" +
"<p>We apologize for the inconvenience</p>");
// Record permanent update failure since this connector is never
// going to succeed
guestService.setApiKeyStatus(apiKey.getId(), ApiKey.Status.STATUS_PERMANENT_FAILURE, null, ApiKey.PermanentFailReason.NEEDS_REAUTH);
throw new UpdateFailedException("refresh token attempt failed", e, true, ApiKey.PermanentFailReason.NEEDS_REAUTH);
}
JSONObject token = JSONObject.fromObject(fetched);
final long expiresIn = token.getLong("expires_in");
final String access_token = token.getString("access_token");
final long now = System.currentTimeMillis();
long tokenExpires = now + (expiresIn*1000);
guestService.setApiKeyAttribute(apiKey,
"accessToken", access_token);
guestService.setApiKeyAttribute(apiKey,
"tokenExpires", String.valueOf(tokenExpires));
}
@RequestMapping(value = "/place/{apiKeyId}/{id}")
public void getMovesPlaceIcon(@PathVariable("apiKeyId") long apiKeyId,
@PathVariable("id") long id,
HttpServletResponse response) throws IOException {
List l = jpaDaoService.executeNativeQuery("SELECT type, foursquareId FROM Facet_MovesPlace WHERE apiKeyId=(?1) AND id=(?2)", apiKeyId, id);
if (l==null||l.size()==0)
response.sendError(404);
final Object[] singleResult = (Object[])l.get(0);
String type = (String) singleResult[0];
if (type.equals("foursquare")) {
String foursquareId = (String) singleResult[1];
final FoursquareVenue foursquareVenue = metadataService.getFoursquareVenue(foursquareId);
response.sendRedirect(foursquareVenue.categoryIconUrlPrefix + "bg_32" + foursquareVenue.categoryIconUrlSuffix);
} else {
String homeBaseUrl = env.get("homeBaseUrl");
response.sendRedirect(homeBaseUrl+"/images/moves/" + type + ".png");
}
}
}