// Copyright 2013 Michel Kraemer // // 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 de.undercouch.citeproc.mendeley; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import de.undercouch.citeproc.csl.CSLItemData; import de.undercouch.citeproc.helper.oauth.OAuth; import de.undercouch.citeproc.helper.oauth.OAuth.Method; import de.undercouch.citeproc.helper.oauth.OAuth2; import de.undercouch.citeproc.helper.oauth.Response; import de.undercouch.citeproc.remote.AbstractRemoteConnector; /** * Reads documents from the Mendeley REST services. Needs an OAuth API key and * secret in order to authenticate. Users of this class should * <a href="http://dev.mendeley.com/">register their app</a> to receive such a * key and secret. * @author Michel Kraemer */ public class MendeleyConnector extends AbstractRemoteConnector { private static final String OAUTH_ACCESS_TOKEN_URL = "https://api.mendeley.com/oauth/token"; private static final String OAUTH_AUTHORIZATION_URL_TEMPLATE = "https://api.mendeley.com/oauth/authorize?redirect_uri=%s" + "&response_type=code&scope=all&client_id=%s"; /** * The REST end-point used to request a Mendeley user's documents */ private static final String MENDELEY_DOCUMENTS_ENDPOINT = "https://api.mendeley.com/documents/"; /** * The default headers to send with every request */ private static final Map<String, String> DEFAULT_HEADERS; static { DEFAULT_HEADERS = new HashMap<>(); // Mendeley API version 1 DEFAULT_HEADERS.put("Accept", "application/vnd.mendeley-document.1+json"); } /** * The response header field containing links to next and previous pages */ private static final String LINK = "Link"; /** * Parameter to get all attributes of a document */ private static final String VIEWALL = "view=all"; /** * The remote service's authorization end-point */ private final String oauthAuthorizationUrl; /** * A cache for retrieved documents */ private Map<String, CSLItemData> cachedDocuments; /** * Constructs a new connector * @param clientId the Mendeley app's client ID * @param clientSecret the app's client secret * @param redirectUri the location users are redirected to after * they granted the Mendeley app access to their library */ public MendeleyConnector(String clientId, String clientSecret, String redirectUri) { super(clientId, clientSecret, redirectUri); try { oauthAuthorizationUrl = String.format( OAUTH_AUTHORIZATION_URL_TEMPLATE, URLEncoder.encode(redirectUri, "UTF-8"), clientId); } catch (UnsupportedEncodingException e) { //should never happen throw new RuntimeException(e); } } @Override protected String getOAuthRequestTokenURL() { //we don't need this for OAuth v2 return null; } @Override protected String getOAuthAuthorizationURL() { return oauthAuthorizationUrl; } @Override protected String getOAuthAccessTokenURL() { return OAUTH_ACCESS_TOKEN_URL; } @Override protected Method getOAuthAccessTokenMethod() { return Method.POST; } @Override protected OAuth createOAuth(String consumerKey, String consumerSecret, String redirectUri) { return new OAuth2(consumerKey, consumerSecret, redirectUri); } private void addToCache(String id, CSLItemData doc) { if (cachedDocuments == null) { cachedDocuments = new HashMap<>(); } cachedDocuments.put(id, doc); } private CSLItemData getFromCache(String id) { if (cachedDocuments == null) { return null; } return cachedDocuments.get(id); } private void clearCache() { if (cachedDocuments != null) { cachedDocuments = null; } } @Override public List<String> getItemIDs() throws IOException { String nextLink = MENDELEY_DOCUMENTS_ENDPOINT + "?" + VIEWALL; List<String> result = new ArrayList<>(); clearCache(); while (nextLink != null) { Response response = performRequest(nextLink, DEFAULT_HEADERS); // get link to next page from response nextLink = null; List<String> linkHeaders = response.getHeaders(LINK); for (String linkHeader : linkHeaders) { String[] linksHeaderParts = linkHeader.split("\\s*,\\s*"); for (String linksHeaderPart : linksHeaderParts) { String[] parts = linksHeaderPart.split("\\s*;\\s*"); if (parts.length > 1 && parts[1].equals("rel=\"next\"")) { nextLink = parts[0]; break; } } if (nextLink != null) { if (nextLink.charAt(0) == '<' && nextLink.charAt(nextLink.length() - 1) == '>') { nextLink = nextLink.substring(1, nextLink.length() - 1); } break; } } // get documents from response try (InputStream is = response.getInputStream()) { List<Object> docsArr = parseResponseArray(response); for (Object docObj : docsArr) { if (docObj instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> docMap = (Map<String, Object>)docObj; String id = docMap.get("id").toString(); CSLItemData doc = MendeleyConverter.convert(id, docMap); result.add(id); addToCache(id, doc); } } } } return result; } @Override public CSLItemData getItem(String itemId) throws IOException { CSLItemData item = getFromCache(itemId); if (item == null) { Map<String, Object> response = performRequestObject( MENDELEY_DOCUMENTS_ENDPOINT + itemId + "?" + VIEWALL, null); item = MendeleyConverter.convert(itemId, response); addToCache(itemId, item); } return item; } @Override public Map<String, CSLItemData> getItems(List<String> itemIds) throws IOException { Map<String, CSLItemData> result = new LinkedHashMap<>(itemIds.size()); for (String id : itemIds) { result.put(id, getItem(id)); } return result; } @Override public int getMaxBulkItems() { return 1; } }