package com.evernote.client.android.asyncclient;
import android.content.Context;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.evernote.client.android.EvernoteSession;
import com.evernote.client.android.EvernoteUtil;
import com.evernote.client.android.helper.EvernotePreconditions;
import com.evernote.client.conn.mobile.ByteStore;
import com.evernote.client.conn.mobile.DiskBackedByteStore;
import com.evernote.client.conn.mobile.TAndroidTransport;
import com.evernote.edam.error.EDAMNotFoundException;
import com.evernote.edam.error.EDAMSystemException;
import com.evernote.edam.error.EDAMUserException;
import com.evernote.edam.notestore.NoteStore;
import com.evernote.edam.type.LinkedNotebook;
import com.evernote.edam.type.User;
import com.evernote.edam.userstore.AuthenticationResult;
import com.evernote.edam.userstore.UserStore;
import com.evernote.thrift.TException;
import com.evernote.thrift.protocol.TBinaryProtocol;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.OkHttpClient;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
/**
* A factory to create async wrappers around a {@link NoteStore.Client}. Use the corresponding
* {@link EvernoteClientFactory.Builder} to create an instance.
* <p/>
* <br>
* <br>
* <p/>
* Try to reuse a created instances. A factory caches created {@link NoteStore.Client}s, their wrappers
* and internal helper objects like the http client. The easiest way to get access to a factory is to
* call {@link EvernoteSession#getEvernoteClientFactory()}.
*
* @author rwondratschek
*/
@SuppressWarnings("unused")
public class EvernoteClientFactory {
protected final EvernoteSession mEvernoteSession;
protected final OkHttpClient mHttpClient;
protected final ByteStore mByteStore;
protected final Map<String, String> mHeaders;
protected final ExecutorService mExecutorService;
private final Map<String, EvernoteUserStoreClient> mUserStoreClients;
private final Map<String, EvernoteNoteStoreClient> mNoteStoreClients;
private final Map<String, EvernoteLinkedNotebookHelper> mLinkedNotebookHelpers;
private EvernoteBusinessNotebookHelper mBusinessNotebookHelper;
private EvernoteHtmlHelper mHtmlHelperDefault;
private final Map<String, EvernoteHtmlHelper> mLinkedHtmlHelper;
private EvernoteHtmlHelper mHtmlHelperBusiness;
private EvernoteSearchHelper mEvernoteSearchHelper;
private final EvernoteAsyncClient mCreateHelperClient;
private com.evernote.edam.userstore.AuthenticationResult mBusinessAuthenticationResult;
protected EvernoteClientFactory(EvernoteSession session, OkHttpClient httpClient, ByteStore byteStore, Map<String, String> headers, ExecutorService executorService) {
mEvernoteSession = EvernotePreconditions.checkNotNull(session);
mHttpClient = EvernotePreconditions.checkNotNull(httpClient);
mByteStore = EvernotePreconditions.checkNotNull(byteStore);
mHeaders = headers;
mExecutorService = EvernotePreconditions.checkNotNull(executorService);
mUserStoreClients = new HashMap<>();
mNoteStoreClients = new HashMap<>();
mLinkedNotebookHelpers = new HashMap<>();
mLinkedHtmlHelper = new HashMap<>();
mCreateHelperClient = new EvernoteAsyncClient(mExecutorService) { };
}
/**
* @return The default client for this session. It references the signed in user's user store.
* @see UserStore
* @see UserStore.Client
*/
public synchronized EvernoteUserStoreClient getUserStoreClient() {
checkLoggedIn();
String url = new Uri.Builder()
.scheme("https")
.authority(mEvernoteSession.getAuthenticationResult().getEvernoteHost())
.path("/edam/user")
.build()
.toString();
return getUserStoreClient(url, mEvernoteSession.getAuthToken());
}
/**
* @return An async wrapper for {@link UserStore.Client} with this specific url and authentication
* token combination.
* @see UserStore
* @see UserStore.Client
*/
public synchronized EvernoteUserStoreClient getUserStoreClient(@NonNull String url, @Nullable String authToken) {
String key = createKey(url, authToken);
EvernoteUserStoreClient userStoreClient = mUserStoreClients.get(key);
if (userStoreClient == null) {
userStoreClient = createUserStoreClient(url, authToken);
mUserStoreClients.put(key, userStoreClient);
}
return userStoreClient;
}
protected EvernoteUserStoreClient createUserStoreClient(String url, String authToken) {
UserStore.Client client = new UserStore.Client(createBinaryProtocol(url));
return new EvernoteUserStoreClient(client, authToken, mExecutorService);
}
/**
* @return The default client for this session. It references the user's private note store.
* @see EvernoteClientFactory#getNoteStoreClient(String, String)
* @see com.evernote.client.android.AuthenticationResult#getNoteStoreUrl()
*/
public synchronized EvernoteNoteStoreClient getNoteStoreClient() {
checkLoggedIn();
return getNoteStoreClient(mEvernoteSession.getAuthenticationResult().getNoteStoreUrl(), EvernotePreconditions.checkNotEmpty(mEvernoteSession.getAuthToken()));
}
/**
* @param url The note store URL.
* @param authToken The authentication token to get access to this note store.
* @return An async wrapper for {@link NoteStore.Client} with this specific url and authentication
* token combination.
* @see NoteStore
* @see NoteStore.Client
*/
public synchronized EvernoteNoteStoreClient getNoteStoreClient(@NonNull String url, @NonNull String authToken) {
String key = createKey(url, authToken);
EvernoteNoteStoreClient client = mNoteStoreClients.get(key);
if (client == null) {
client = createEvernoteNoteStoreClient(url, authToken);
mNoteStoreClients.put(key, client);
}
return client;
}
/**
* Returns an async wrapper providing several helper methods for this {@link LinkedNotebook}. With
* {@link EvernoteLinkedNotebookHelper#getClient()} you can get access to the underlying {@link EvernoteNoteStoreClient},
* which references the {@link LinkedNotebook}'s note store URL.
*
* @param linkedNotebook The referenced {@link LinkedNotebook}. Its GUID and share key must not be
* {@code null}.
* @return An async wrapper providing several helper methods.
*/
public synchronized EvernoteLinkedNotebookHelper getLinkedNotebookHelper(@NonNull LinkedNotebook linkedNotebook)
throws EDAMUserException, EDAMSystemException, EDAMNotFoundException, TException {
String key = linkedNotebook.getGuid();
EvernoteLinkedNotebookHelper notebookHelper = mLinkedNotebookHelpers.get(key);
if (notebookHelper == null) {
notebookHelper = createLinkedNotebookHelper(linkedNotebook);
mLinkedNotebookHelpers.put(key, notebookHelper);
}
return notebookHelper;
}
/**
* @see #getLinkedNotebookHelper(LinkedNotebook)
*/
public Future<EvernoteLinkedNotebookHelper> getLinkedNotebookHelperAsync(@NonNull final LinkedNotebook linkedNotebook,
@Nullable EvernoteCallback<EvernoteLinkedNotebookHelper> callback) {
return mCreateHelperClient.submitTask(new Callable<EvernoteLinkedNotebookHelper>() {
@Override
public EvernoteLinkedNotebookHelper call() throws Exception {
return getLinkedNotebookHelper(linkedNotebook);
}
}, callback);
}
protected EvernoteLinkedNotebookHelper createLinkedNotebookHelper(@NonNull LinkedNotebook linkedNotebook) throws EDAMUserException,
EDAMSystemException, EDAMNotFoundException, TException {
String url = linkedNotebook.getNoteStoreUrl();
EvernoteNoteStoreClient client = getNoteStoreClient(url, EvernotePreconditions.checkNotEmpty(mEvernoteSession.getAuthToken()));
AuthenticationResult authenticationResult = client.authenticateToSharedNotebook(linkedNotebook.getShareKey());
client = getNoteStoreClient(url, authenticationResult.getAuthenticationToken());
return new EvernoteLinkedNotebookHelper(client, linkedNotebook, mExecutorService);
}
/**
* Returns an async wrapper providing several helper methods for business notebooks. With
* {@link EvernoteBusinessNotebookHelper#getClient()} you can get access to the underlying {@link EvernoteNoteStoreClient},
* which references the business note store URL.
*
* @return An async wrapper providing several helper methods.
*/
public synchronized EvernoteBusinessNotebookHelper getBusinessNotebookHelper() throws TException, EDAMUserException, EDAMSystemException {
if (mBusinessNotebookHelper == null || isBusinessAuthExpired()) {
mBusinessNotebookHelper = createBusinessNotebookHelper();
}
return mBusinessNotebookHelper;
}
/**
* @see #getBusinessNotebookHelper()
*/
public Future<EvernoteBusinessNotebookHelper> getBusinessNotebookHelperAsync(@Nullable EvernoteCallback<EvernoteBusinessNotebookHelper> callback) {
return mCreateHelperClient.submitTask(new Callable<EvernoteBusinessNotebookHelper>() {
@Override
public EvernoteBusinessNotebookHelper call() throws Exception {
return getBusinessNotebookHelper();
}
}, callback);
}
protected EvernoteBusinessNotebookHelper createBusinessNotebookHelper() throws TException, EDAMUserException, EDAMSystemException {
authenticateToBusiness();
EvernoteNoteStoreClient client = getNoteStoreClient(mBusinessAuthenticationResult.getNoteStoreUrl(), mBusinessAuthenticationResult.getAuthenticationToken());
User businessUser = mBusinessAuthenticationResult.getUser();
return new EvernoteBusinessNotebookHelper(client, mExecutorService, businessUser.getUsername(), businessUser.getShardId());
}
/**
* Use this method, if you want to download a personal note as HTML.
*
* @return An async wrapper to load a note as HTML from the Evernote service.
*/
public synchronized EvernoteHtmlHelper getHtmlHelperDefault() {
checkLoggedIn();
if (mHtmlHelperDefault == null) {
mHtmlHelperDefault = createHtmlHelper(mEvernoteSession.getAuthToken());
}
return mHtmlHelperDefault;
}
/**
* Use this method, if you want to download a linked note as HTML.
*
* @param linkedNotebook The referenced {@link LinkedNotebook}. Its GUID and share key must not be
* {@code null}.
* @return An async wrapper to load a note as HTML from the Evernote service.
*/
public EvernoteHtmlHelper getLinkedHtmlHelper(@NonNull LinkedNotebook linkedNotebook) throws EDAMUserException, EDAMSystemException, EDAMNotFoundException, TException {
String key = linkedNotebook.getGuid();
EvernoteHtmlHelper htmlHelper = mLinkedHtmlHelper.get(key);
if (htmlHelper == null) {
String url = linkedNotebook.getNoteStoreUrl();
EvernoteNoteStoreClient client = getNoteStoreClient(url, EvernotePreconditions.checkNotEmpty(mEvernoteSession.getAuthToken()));
AuthenticationResult authenticationResult = client.authenticateToSharedNotebook(linkedNotebook.getShareKey());
htmlHelper = createHtmlHelper(authenticationResult.getAuthenticationToken());
mLinkedHtmlHelper.put(key, htmlHelper);
}
return htmlHelper;
}
/**
* @see #getLinkedNotebookHelper(LinkedNotebook)
*/
public Future<EvernoteHtmlHelper> getLinkedHtmlHelperAsync(@NonNull final LinkedNotebook linkedNotebook, @Nullable EvernoteCallback<EvernoteHtmlHelper> callback) {
return mCreateHelperClient.submitTask(new Callable<EvernoteHtmlHelper>() {
@Override
public EvernoteHtmlHelper call() throws Exception {
return getLinkedHtmlHelper(linkedNotebook);
}
}, callback);
}
/**
* Use this method, if you want to download a business note as HTML.
*
* @return An async wrapper to load a business note as HTML from the Evernote service.
*/
public synchronized EvernoteHtmlHelper getHtmlHelperBusiness() throws TException, EDAMUserException, EDAMSystemException {
if (mHtmlHelperBusiness == null) {
authenticateToBusiness();
mHtmlHelperBusiness = createHtmlHelper(mBusinessAuthenticationResult.getAuthenticationToken());
}
return mHtmlHelperBusiness;
}
/**
* @see #getHtmlHelperBusiness()
*/
public Future<EvernoteHtmlHelper> getHtmlHelperBusinessAsync(@Nullable EvernoteCallback<EvernoteHtmlHelper> callback) {
return mCreateHelperClient.submitTask(new Callable<EvernoteHtmlHelper>() {
@Override
public EvernoteHtmlHelper call() throws Exception {
return getHtmlHelperBusiness();
}
}, callback);
}
protected EvernoteHtmlHelper createHtmlHelper(String authToken) {
return new EvernoteHtmlHelper(mHttpClient, mEvernoteSession.getAuthenticationResult().getEvernoteHost(), authToken, mExecutorService);
}
/**
* @return An async wrapper to search notes in multiple note stores.
*/
public EvernoteSearchHelper getEvernoteSearchHelper() {
checkLoggedIn();
if (mEvernoteSearchHelper == null) {
mEvernoteSearchHelper = createEvernoteSearchHelper();
}
return mEvernoteSearchHelper;
}
protected EvernoteSearchHelper createEvernoteSearchHelper() {
return new EvernoteSearchHelper(mEvernoteSession, mExecutorService);
}
protected TBinaryProtocol createBinaryProtocol(String url) {
return new TBinaryProtocol(new TAndroidTransport(mHttpClient, mByteStore, url, mHeaders));
}
protected NoteStore.Client createNoteStoreClient(String url) {
return new NoteStore.Client(createBinaryProtocol(url));
}
protected synchronized EvernoteNoteStoreClient createEvernoteNoteStoreClient(String url, String authToken) {
return new EvernoteNoteStoreClient(createNoteStoreClient(url), authToken, mExecutorService);
}
protected final String createKey(String url, String authToken) {
if (url == null && authToken == null) {
throw new IllegalArgumentException();
} else if (url == null) {
return authToken;
} else if (authToken == null) {
return url;
} else {
return url + authToken;
}
}
protected final void authenticateToBusiness() throws TException, EDAMUserException, EDAMSystemException {
if (isBusinessAuthExpired()) {
mBusinessAuthenticationResult = getUserStoreClient().authenticateToBusiness();
}
}
protected final boolean isBusinessAuthExpired() {
return mBusinessAuthenticationResult == null || mBusinessAuthenticationResult.getExpiration() < System.currentTimeMillis();
}
protected void checkLoggedIn() {
if (!mEvernoteSession.isLoggedIn()) {
throw new IllegalStateException("user not logged in");
}
}
/**
* A builder to construct an {@link EvernoteClientFactory}. The recommended approach is to set
* the builder in the session with {@link EvernoteSession#setEvernoteClientFactoryBuilder(Builder)}
* and then to call {@link EvernoteSession#getEvernoteClientFactory()}.
*/
public static class Builder {
private final EvernoteSession mEvernoteSession;
private final Map<String, String> mHeaders;
private OkHttpClient mHttpClient;
private ByteStore.Factory mByteStoreFactory;
private ExecutorService mExecutorService;
/**
* @param evernoteSession The current session, must not be {@code null}.
*/
public Builder(EvernoteSession evernoteSession) {
mEvernoteSession = EvernotePreconditions.checkNotNull(evernoteSession);
mHeaders = new HashMap<>();
}
/**
* @param httpClient The client executing the HTTP calls.
*/
public Builder setHttpClient(OkHttpClient httpClient) {
mHttpClient = httpClient;
return this;
}
/**
* @param byteStoreFactory Creates the {@link ByteStore} for each Thread. The {@link ByteStore}
* caches the written data, which is later sent to the Evernote service.
*/
public Builder setByteStoreFactory(ByteStore.Factory byteStoreFactory) {
mByteStoreFactory = byteStoreFactory;
return this;
}
private Builder addHeader(String name, String value) {
// maybe set this to public
mHeaders.put(name, value);
return this;
}
/**
* @param executorService Runs the background actions.
*/
public Builder setExecutorService(ExecutorService executorService) {
mExecutorService = executorService;
return this;
}
public EvernoteClientFactory build() {
if (mHttpClient == null) {
mHttpClient = createDefaultHttpClient();
}
if (mByteStoreFactory == null) {
mByteStoreFactory = createDefaultByteStore(mEvernoteSession.getApplicationContext());
}
if (mExecutorService == null) {
mExecutorService = Executors.newSingleThreadExecutor();
}
addHeader("Cache-Control", "no-transform");
addHeader("Accept", "application/x-thrift");
addHeader("User-Agent", EvernoteUtil.generateUserAgentString(mEvernoteSession.getApplicationContext()));
return new EvernoteClientFactory(mEvernoteSession, mHttpClient, mByteStoreFactory.create(), mHeaders, mExecutorService);
}
private OkHttpClient createDefaultHttpClient() {
OkHttpClient httpClient = new OkHttpClient();
httpClient.setConnectTimeout(10, TimeUnit.SECONDS);
httpClient.setReadTimeout(10, TimeUnit.SECONDS);
httpClient.setWriteTimeout(20, TimeUnit.SECONDS);
httpClient.setConnectionPool(new ConnectionPool(20, 2 * 60 * 1000));
return httpClient;
}
private ByteStore.Factory createDefaultByteStore(Context context) {
int cacheSize = (int) (Runtime.getRuntime().maxMemory() / 32);
return new DiskBackedByteStore.Factory(new File(context.getCacheDir(), "evernoteCache"), cacheSize);
}
}
}