package com.evernote.client.android.asyncclient; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Pair; import com.evernote.client.android.EvernoteSession; import com.evernote.client.android.helper.EvernotePreconditions; import com.evernote.client.android.type.NoteRef; import com.evernote.edam.error.EDAMNotFoundException; import com.evernote.edam.error.EDAMSystemException; import com.evernote.edam.error.EDAMUserException; import com.evernote.edam.notestore.NoteFilter; import com.evernote.edam.notestore.NoteMetadata; import com.evernote.edam.notestore.NotesMetadataList; import com.evernote.edam.notestore.NotesMetadataResultSpec; import com.evernote.edam.type.LinkedNotebook; import com.evernote.edam.type.NoteSortOrder; import com.evernote.edam.type.Notebook; import com.evernote.thrift.TException; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; /** * Provides an unified search method to look for notes in multiple note stores. * * <br> * <br> * * <b>Be careful</b> using this class. A query may be very intense and could require multiple requests. * * <br> * <br> * * The easiest way to create an instance is to call {@link EvernoteClientFactory#getEvernoteSearchHelper()}. * * @author rwondratschek */ @SuppressWarnings("unused") public class EvernoteSearchHelper extends EvernoteAsyncClient { private final EvernoteSession mSession; private final EvernoteClientFactory mClientFactory; private final EvernoteNoteStoreClient mPrivateClient; /** * @param session The current valid session. * @param executorService The executor running the actions in the background. */ public EvernoteSearchHelper(@NonNull EvernoteSession session, @NonNull ExecutorService executorService) { super(executorService); mSession = EvernotePreconditions.checkNotNull(session); mClientFactory = mSession.getEvernoteClientFactory(); mPrivateClient = mClientFactory.getNoteStoreClient(); } /** * Submits a search. * * @param search The desired search with its parameters. * @return The result containing multiple {@link NotesMetadataList}s. */ public Result execute(@NonNull Search search) throws Exception { if (search.getOffset() >= search.getMaxNotes()) { throw new IllegalArgumentException("offset must be less than max notes"); } Result result = new Result(search.getScopes()); for (Scope scope : search.getScopes()) { switch (scope) { case PERSONAL_NOTES: try { result.setPersonalResults(findPersonalNotes(search)); } catch (Exception e) { maybeRethrow(search, e); } break; case LINKED_NOTEBOOKS: List<LinkedNotebook> linkedNotebooks = getLinkedNotebooks(search, false); for (LinkedNotebook linkedNotebook : linkedNotebooks) { try { result.addLinkedNotebookResult(linkedNotebook, findNotesInLinkedNotebook(search, linkedNotebook)); } catch (Exception e) { maybeRethrow(search, e); } } break; case BUSINESS: linkedNotebooks = getLinkedNotebooks(search, true); for (LinkedNotebook linkedNotebook : linkedNotebooks) { try { result.addBusinessResult(linkedNotebook, findNotesInBusinessNotebook(search, linkedNotebook)); } catch (Exception e) { maybeRethrow(search, e); } } break; } } return result; } /** * @see #execute(Search) */ public Future<Result> executeAsync(@NonNull final Search search, @Nullable EvernoteCallback<Result> callback) { return submitTask(new Callable<Result>() { @Override public Result call() throws Exception { return execute(search); } }, callback); } protected List<NotesMetadataList> findPersonalNotes(Search search) throws Exception { return findAllNotes(search, mPrivateClient, search.getNoteFilter()); } protected List<NotesMetadataList> findNotesInLinkedNotebook(Search search, LinkedNotebook linkedNotebook) throws Exception { EvernoteLinkedNotebookHelper linkedNotebookHelper = mClientFactory.getLinkedNotebookHelper(linkedNotebook); Notebook correspondingNotebook = linkedNotebookHelper.getCorrespondingNotebook(); // create a deep copy so that we don't touch the initial search request values NoteFilter noteFilter = new NoteFilter(search.getNoteFilter()); noteFilter.setNotebookGuid(correspondingNotebook.getGuid()); return findAllNotes(search, linkedNotebookHelper.getClient(), noteFilter); } protected List<NotesMetadataList> findNotesInBusinessNotebook(Search search, LinkedNotebook linkedNotebook) throws Exception { EvernoteBusinessNotebookHelper businessNotebookHelper = mClientFactory.getBusinessNotebookHelper(); EvernoteLinkedNotebookHelper linkedNotebookHelper = mClientFactory.getLinkedNotebookHelper(linkedNotebook); Notebook correspondingNotebook = linkedNotebookHelper.getCorrespondingNotebook(); // create a deep copy so that we don't touch the initial search request values NoteFilter noteFilter = new NoteFilter(search.getNoteFilter()); noteFilter.setNotebookGuid(correspondingNotebook.getGuid()); return findAllNotes(search, businessNotebookHelper.getClient(), noteFilter); } protected List<NotesMetadataList> findAllNotes(Search search, EvernoteNoteStoreClient client, NoteFilter filter) throws Exception { List<NotesMetadataList> result = new ArrayList<>(); final int maxNotes = search.getMaxNotes(); int offset = search.getOffset(); int remaining = maxNotes - offset; while (remaining > 0) { try { NotesMetadataList notesMetadata = client.findNotesMetadata(filter, offset, maxNotes, search.getResultSpec()); remaining = notesMetadata.getTotalNotes() - (notesMetadata.getStartIndex() + notesMetadata.getNotesSize()); result.add(notesMetadata); } catch (EDAMUserException | EDAMSystemException | TException | EDAMNotFoundException e) { maybeRethrow(search, e); remaining -= search.getPageSize(); } offset += search.getPageSize(); } return result; } protected List<LinkedNotebook> getLinkedNotebooks(Search search, boolean business) throws Exception { if (business) { if (search.mBusinessNotebooks.isEmpty()) { try { return mClientFactory.getBusinessNotebookHelper().listBusinessNotebooks(mSession); } catch (EDAMUserException | EDAMSystemException | EDAMNotFoundException | TException e) { maybeRethrow(search, e); return Collections.emptyList(); } } else { return search.mBusinessNotebooks; } } else { if (search.mLinkedNotebooks.isEmpty()) { try { return mPrivateClient.listLinkedNotebooks(); } catch (EDAMUserException | EDAMNotFoundException | TException | EDAMSystemException e) { maybeRethrow(search, e); return Collections.emptyList(); } } else { return search.mLinkedNotebooks; } } } private void maybeRethrow(Search search, Exception e) throws Exception { if (!search.isIgnoreExceptions()) { throw e; } } /** * Defines from where the notes are queried. */ public enum Scope { PERSONAL_NOTES, LINKED_NOTEBOOKS, BUSINESS } public static class Search { private final EnumSet<Scope> mScopes; private final List<LinkedNotebook> mLinkedNotebooks; private final List<LinkedNotebook> mBusinessNotebooks; private NoteFilter mNoteFilter; private NotesMetadataResultSpec mResultSpec; private int mOffset; private int mMaxNotes; private int mPageSize; private boolean mIgnoreExceptions; public Search() { mScopes = EnumSet.noneOf(Scope.class); mLinkedNotebooks = new ArrayList<>(); mBusinessNotebooks = new ArrayList<>(); mOffset = -1; mMaxNotes = -1; mPageSize = -1; } /** * If no scope is specified, {@link Scope#PERSONAL_NOTES} is the default value. * * <br> * <br> * * <b>Attention:</b> If you add {@link Scope#LINKED_NOTEBOOKS} or {@link Scope#BUSINESS} and * don't add a specific {@link LinkedNotebook}, then this search may be very intense. * * @param scope Add this scope to the search. * @see #addLinkedNotebook(LinkedNotebook) */ public Search addScope(Scope scope) { mScopes.add(scope); return this; } /** * Specify in which notebooks notes are queried. If this linked notebook is a business notebook, * {@link Scope#BUSINESS} is automatically added, otherwise {@link Scope#LINKED_NOTEBOOKS} is * added. * * <br> * <br> * * By default no specific linked notebook is defined. * * @param linkedNotebook The desired linked notebook. * @see #addScope(Scope) */ public Search addLinkedNotebook(LinkedNotebook linkedNotebook) { if (EvernoteBusinessNotebookHelper.isBusinessNotebook(linkedNotebook)) { addScope(Scope.BUSINESS); mBusinessNotebooks.add(linkedNotebook); } else { addScope(Scope.LINKED_NOTEBOOKS); mLinkedNotebooks.add(linkedNotebook); } return this; } /** * If not filter is set, then the default value only sets {@link NoteSortOrder#UPDATED}. * * @param noteFilter The used filter for all queries. */ public Search setNoteFilter(NoteFilter noteFilter) { mNoteFilter = EvernotePreconditions.checkNotNull(noteFilter); return this; } /** * If no spec is set, then the default value will include the note's title and notebook GUID. * * @param resultSpec The used result spec for all queries. */ public Search setResultSpec(NotesMetadataResultSpec resultSpec) { mResultSpec = EvernotePreconditions.checkNotNull(resultSpec); return this; } /** * The default value is {@code 0}. * * @param offset The beginning offset for all queries. */ public Search setOffset(int offset) { mOffset = EvernotePreconditions.checkArgumentNonnegative(offset, "negative value now allowed"); return this; } /** * The default value is {@code 10}. Set this value to {@link Integer#MAX_VALUE}, if you want to query * all notes. The higher this value the more intense is the whole search. * * @param maxNotes The maximum note count for all queries. */ public Search setMaxNotes(int maxNotes) { mMaxNotes = EvernotePreconditions.checkArgumentPositive(maxNotes, "maxNotes must be greater or equal 1"); return this; } /** * The default value is {@code 10}. * * @param pageSize The page size for a single search. */ public Search setPageSize(int pageSize) { mPageSize = EvernotePreconditions.checkArgumentPositive(pageSize, "pageSize must be greater or equal 1"); return this; } /** * The default value is {@code false}. * * @param ignoreExceptions If {@code true} then most exceptions while running the search are caught and ignored. */ public Search setIgnoreExceptions(boolean ignoreExceptions) { mIgnoreExceptions = ignoreExceptions; return this; } private EnumSet<Scope> getScopes() { if (mScopes.isEmpty()) { mScopes.add(Scope.PERSONAL_NOTES); } return mScopes; } private NoteFilter getNoteFilter() { if (mNoteFilter == null) { mNoteFilter = new NoteFilter(); mNoteFilter.setOrder(NoteSortOrder.UPDATED.getValue()); } return mNoteFilter; } private NotesMetadataResultSpec getResultSpec() { if (mResultSpec == null) { mResultSpec = new NotesMetadataResultSpec(); mResultSpec.setIncludeTitle(true); mResultSpec.setIncludeNotebookGuid(true); } return mResultSpec; } // for all scopes private int getOffset() { if (mOffset < 0) { return 0; } return mOffset; } private int getMaxNotes() { if (mMaxNotes < 0) { return 10; } return mMaxNotes; } private int getPageSize() { if (mPageSize < 0) { return 10; } return mPageSize; } public boolean isIgnoreExceptions() { return mIgnoreExceptions; } } /** * A search result. */ public static final class Result { private final List<NotesMetadataList> mPersonalResults; private final Map<Pair<String, LinkedNotebook>, List<NotesMetadataList>> mLinkedNotebookResults; private final Map<Pair<String, LinkedNotebook>, List<NotesMetadataList>> mBusinessResults; private NoteRef.Factory mNoteRefFactory; private Result(Set<Scope> scopes) { mPersonalResults = scopes.contains(Scope.PERSONAL_NOTES) ? new ArrayList<NotesMetadataList>() : null; mLinkedNotebookResults = scopes.contains(Scope.LINKED_NOTEBOOKS) ? new HashMap<Pair<String, LinkedNotebook>, List<NotesMetadataList>>() : null; mBusinessResults = scopes.contains(Scope.BUSINESS) ? new HashMap<Pair<String, LinkedNotebook>, List<NotesMetadataList>>() : null; mNoteRefFactory = new NoteRef.DefaultFactory(); } /** * Exchange the factory to control how the {@link NoteRef} instances are created if you receive * the results as NoteRef. * * @param noteRefFactory The new factory to construct the {@link NoteRef} instances. */ public void setNoteRefFactory(@NonNull NoteRef.Factory noteRefFactory) { mNoteRefFactory = EvernotePreconditions.checkNotNull(noteRefFactory); } private void setPersonalResults(List<NotesMetadataList> personalResults) { mPersonalResults.addAll(personalResults); } private void addLinkedNotebookResult(LinkedNotebook linkedNotebook, List<NotesMetadataList> linkedNotebookResult) { Pair<String, LinkedNotebook> key = new Pair<>(linkedNotebook.getGuid(), linkedNotebook); mLinkedNotebookResults.put(key, linkedNotebookResult); } private void addBusinessResult(LinkedNotebook linkedNotebook, List<NotesMetadataList> linkedNotebookResult) { Pair<String, LinkedNotebook> key = new Pair<>(linkedNotebook.getGuid(), linkedNotebook); mBusinessResults.put(key, linkedNotebookResult); } /** * @return All paginated {@link NotesMetadataList}s containing the personal notes. Returns * {@code null}, if {@link Scope#PERSONAL_NOTES} wasn't set. */ public List<NotesMetadataList> getPersonalResults() { return mPersonalResults; } /** * @return All personal notes. Returns {@code null}, if {@link Scope#PERSONAL_NOTES} wasn't set. */ public List<NoteRef> getPersonalResultsAsNoteRef() { if (mPersonalResults == null) { return null; } List<NoteRef> result = new ArrayList<>(); fillNoteRef(mPersonalResults, result, null); return result; } /** * @return All linked notebooks with their paginated search result. The key in the returned * map consists of the {@link LinkedNotebook} and its GUID. Returns {@code null}, if * {@link Scope#LINKED_NOTEBOOKS} wasn't set. */ public Map<Pair<String, LinkedNotebook>, List<NotesMetadataList>> getLinkedNotebookResults() { return mLinkedNotebookResults; } /** * @return All linked notes. Returns {@code null}, if {@link Scope#LINKED_NOTEBOOKS} wasn't set. */ public List<NoteRef> getLinkedNotebookResultsAsNoteRef() { if (mLinkedNotebookResults == null) { return null; } List<NoteRef> result = new ArrayList<>(); for (Pair<String, LinkedNotebook> key : mLinkedNotebookResults.keySet()) { List<NotesMetadataList> notesMetadataLists = mLinkedNotebookResults.get(key); fillNoteRef(notesMetadataLists, result, key.second); } return result; } /** * @return All business notebooks with their paginated search result. The key in the returned * map consists of the business notebook and its GUID. Returns {@code null}, if * {@link Scope#BUSINESS} wasn't set. */ public Map<Pair<String, LinkedNotebook>, List<NotesMetadataList>> getBusinessResults() { return mBusinessResults; } /** * @return All business notes. Returns {@code null}, if {@link Scope#BUSINESS} wasn't set. */ public List<NoteRef> getBusinessResultsAsNoteRef() { if (mBusinessResults == null) { return null; } List<NoteRef> result = new ArrayList<>(); for (Pair<String, LinkedNotebook> key : mBusinessResults.keySet()) { List<NotesMetadataList> notesMetadataLists = mBusinessResults.get(key); fillNoteRef(notesMetadataLists, result, key.second); } return result; } /** * @return All personal, linked and business notes. Never returns {@code null}, if no results * were found then the list is empty. */ public List<NoteRef> getAllAsNoteRef() { List<NoteRef> result = new ArrayList<>(); List<NoteRef> part = getPersonalResultsAsNoteRef(); if (part != null) { result.addAll(part); } part = getLinkedNotebookResultsAsNoteRef(); if (part != null) { result.addAll(part); } part = getBusinessResultsAsNoteRef(); if (part != null) { result.addAll(part); } return result; } protected void fillNoteRef(final List<NotesMetadataList> notesMetadataList, final List<NoteRef> result, LinkedNotebook linkedNotebook) { for (NotesMetadataList notesMetadataListEntry : notesMetadataList) { List<NoteMetadata> notes = notesMetadataListEntry.getNotes(); for (NoteMetadata note : notes) { NoteRef ref = linkedNotebook == null ? mNoteRefFactory.fromPersonal(note) : mNoteRefFactory.fromLinked(note, linkedNotebook); result.add(ref); } } } } }