/*
* Copyright (c) 2007-2014 by Public Library of Science
*
* http://plos.org
* http://ambraproject.org
*
* 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 org.ambraproject.service.orcid;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.ambraproject.views.OrcidAuthorization;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.RequestEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.URLEncoder;
/**
* {@inheritDoc}
*/
public class OrcidServiceImpl implements OrcidService {
private static final Logger log = LoggerFactory.getLogger(OrcidServiceImpl.class);
private String tokenEndPoint;
private HttpClient httpClient;
private String clientID;
private String clientSecret;
/**
* This sets the access level to request of ORCiD
*
* http://support.orcid.org/knowledgebase/articles/120162-orcid-scopes
*
* We'll only want to READ data so I've chosen: "/orcid-profile/read-limited"
*
*/
public static String API_SCOPE = "/orcid-profile/read-limited";
private Gson gson;
public OrcidServiceImpl()
{
/**
* This never changes, lets only create it once.
*/
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(OrcidAuthorization.class, new JsonDeserializer<OrcidAuthorization>() {
@Override
public OrcidAuthorization deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
JsonObject json = (JsonObject)jsonElement;
return OrcidAuthorization.builder()
.setAccessToken(json.get("access_token").getAsString())
.setTokenType(json.get("token_type").getAsString())
.setRefreshToken(json.get("refresh_token").getAsString())
.setExpiresIn(json.get("expires_in").getAsInt())
.setScope(json.get("scope").getAsString())
.setOrcid(json.get("orcid").getAsString())
.build();
}
});
gson = gsonBuilder.create();
}
/**
* {@inheritDoc}
*/
public OrcidAuthorization authorizeUser(String authorizationCode) throws Exception
{
//Currently we authorize the account, but all we really want is the ORCiD
//which comes down along with the authorization token, so we
//never actually have to use the returned accessToken. However
//I am capturing it for future use.
PostMethod post = createOrcidAccessTokenQuery(authorizationCode);
try {
long timestamp = System.currentTimeMillis();
int response = httpClient.executeMethod(post);
log.debug("Http post finished in {} ms", System.currentTimeMillis() - timestamp);
//The error handling here is a bit weird
//ORCiD will return a 500 error if an invalid token is present
//It will also return this code if the application throws an exception on their end
//The only way to seemingly disambiguate would be to parse the response text if we
//want to improve on this later
//For now, I log the error and return null.
//ORCiD will return 401 when an invalid client-id or client-secret is present
//ORCiD returns 200 on success
if (response == 200) {
String result = post.getResponseBodyAsString();
if(result != null) {
/** Example response:
{
"access_token": "8056b6d5-f96d-42c8-8df9-41f70908ad33",
"token_type": "bearer",
"refresh_token": "d8c72880-00ac-4447-a2af-e90f1673f4bc",
"expires_in": 631138286,
"scope": "/orcid-profile/read-limited",
"orcid": "0000-0003-4954-7894"
}*/
log.trace("Response received: {}", result);
return gson.fromJson(result, OrcidAuthorization.class);
}
log.error("Received empty response, response code {}, when executing query {}", response, post.getURI().toString());
throw new IOException("Received empty response from ORCiD");
} else if (response == 401) {
throw new OrcidAuthorizationException("Invalid client ID or secret not defined for ORCiD, check your ambra configuration");
} else {
log.error("Received response code {} when executing query {}", response, post.getURI().toString());
log.error("Received response body: {}", post.getResponseBodyAsString());
return null;
}
} finally {
// be sure the connection is released back to the connection manager
post.releaseConnection();
}
}
private PostMethod createOrcidAccessTokenQuery(String authorizationCode) throws UnsupportedEncodingException {
final String query = "code=" + authorizationCode +
"&client_id=" + this.clientID +
"&scope=" + URLEncoder.encode(API_SCOPE, "UTF-8") +
"&client_secret=" + this.clientSecret +
"&grant_type=authorization_code";
return new PostMethod(this.tokenEndPoint) {{
setRequestEntity(new RequestEntity() {
@Override
public boolean isRepeatable() {
return false;
}
@Override
public void writeRequest(OutputStream outputStream) throws IOException {
outputStream.write(query.getBytes());
}
@Override
public long getContentLength() {
return query.getBytes().length;
}
@Override
public String getContentType() {
return "application/x-www-form-urlencoded";
}
});
}};
}
public void setTokenEndPoint(String tokenEndPoint) {
this.tokenEndPoint = tokenEndPoint;
}
@Required
public void setHttpClient(HttpClient httpClient) {
this.httpClient = httpClient;
}
public void setClientID(String clientID) {
this.clientID = clientID;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
/**
* {@inheritDoc}
*/
public String getClientID() {
return clientID;
}
/**
* {@inheritDoc}
*/
public String getScope() {
return API_SCOPE;
}
}