package org.fluxtream.connectors.evernote;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.evernote.auth.EvernoteAuth;
import com.evernote.auth.EvernoteService;
import com.evernote.clients.ClientFactory;
import com.evernote.clients.NoteStoreClient;
import com.evernote.edam.error.EDAMErrorCode;
import com.evernote.edam.error.EDAMSystemException;
import com.evernote.edam.error.EDAMUserException;
import com.evernote.edam.notestore.SyncChunk;
import com.evernote.edam.notestore.SyncState;
import com.evernote.edam.type.Data;
import com.evernote.edam.type.Note;
import com.evernote.edam.type.NoteAttributes;
import com.evernote.edam.type.NoteSortOrder;
import com.evernote.edam.type.Notebook;
import com.evernote.edam.type.Publishing;
import com.evernote.edam.type.Resource;
import com.evernote.edam.type.ResourceAttributes;
import com.evernote.edam.type.Tag;
import com.evernote.thrift.TException;
import org.codehaus.plexus.util.ExceptionUtils;
import org.fluxtream.core.aspects.FlxLogger;
import org.fluxtream.core.connectors.Connector;
import org.fluxtream.core.connectors.annotations.Updater;
import org.fluxtream.core.connectors.location.LocationFacet;
import org.fluxtream.core.connectors.updaters.AbstractUpdater;
import org.fluxtream.core.connectors.updaters.AuthExpiredException;
import org.fluxtream.core.connectors.updaters.RateLimitReachedException;
import org.fluxtream.core.connectors.updaters.SettingsAwareUpdater;
import org.fluxtream.core.connectors.updaters.SharedConnectorSettingsAwareUpdater;
import org.fluxtream.core.connectors.updaters.UpdateInfo;
import org.fluxtream.core.domain.ApiKey;
import org.fluxtream.core.domain.ChannelMapping;
import org.fluxtream.core.domain.SharedConnector;
import org.fluxtream.core.services.ApiDataService;
import org.fluxtream.core.services.BuddiesService;
import org.fluxtream.core.services.JPADaoService;
import org.fluxtream.core.services.MetadataService;
import org.fluxtream.core.services.SettingsService;
import org.fluxtream.core.services.impl.BodyTrackHelper;
import org.fluxtream.core.utils.JPAUtils;
import com.syncthemall.enml4j.ENMLProcessor;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.tika.mime.MimeType;
import org.apache.tika.mime.MimeTypeException;
import org.apache.tika.mime.MimeTypes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
*
* @author Candide Kemmler (candide@fluxtream.com)
*/
@Component
@Updater(prettyName = "Evernote", value = 17, objectTypes ={LocationFacet.class, EvernoteNoteFacet.class,
EvernoteTagFacet.class, EvernoteNotebookFacet.class,
EvernoteResourceFacet.class, EvernotePhotoFacet.class},
settings = EvernoteConnectorSettings.class, bodytrackResponder = EvernoteBodytrackResponder.class,
defaultChannels = {"Evernote.photo", "Evernote.note"},
deleteOrder={1, 2, 4, 8, 32, 16},
sharedConnectorFilter = EvernoteSharedConnectorFilter.class)
public class EvernoteUpdater extends AbstractUpdater implements SettingsAwareUpdater, SharedConnectorSettingsAwareUpdater {
public static final String MAIN_APPENDIX = "main";
public static final String RECOGNITION_APPENDIX = "recognition";
public static final String ALTERNATE_APPENDIX = "alternate";
public static final String EVERNOTE_DEFAULT_BGCOLOR = "#82B652";
FlxLogger logger = FlxLogger.getLogger(EvernoteUpdater.class);
private static final int MAX_ENTRIES = 200;
private static final String LAST_UPDATE_COUNT = "evernoteLastUpdateCount";
private static final String LAST_SYNC_TIME = "evernoteLastSyncTime";
@Autowired
BodyTrackHelper bodyTrackHelper;
@Autowired
JPADaoService jpaDaoService;
@Autowired
MetadataService metadataService;
@Autowired
SettingsService settingsService;
@Autowired
BuddiesService buddiesService;
ENMLProcessor processor = new ENMLProcessor();
static {
System.setProperty("javax.xml.stream.XMLInputFactory", "com.ctc.wstx.stax.WstxInputFactory");
System.setProperty("javax.xml.stream.XMLOutputFactory", "com.ctc.wstx.stax.WstxOutputFactory");
System.setProperty("javax.xml.stream.XMLEventFactory", "com.ctc.wstx.stax.WstxEventFactory");
}
@Override
protected void updateConnectorDataHistory(final UpdateInfo updateInfo) throws Exception {
final NoteStoreClient noteStore = getNoteStoreClient(updateInfo);
performSync(updateInfo, noteStore, true);
}
private void resetChannelMapping(final UpdateInfo updateInfo) {
final ApiKey apiKey = guestService.getApiKey(updateInfo.apiKey.getId());
final EvernoteConnectorSettings connectorSettings = (EvernoteConnectorSettings)
syncConnectorSettings(updateInfo, settingsService.getConnectorSettings(updateInfo.apiKey.getId()));
setChannelMapping(apiKey, connectorSettings.notebooks);
}
@Override
protected void updateConnectorData(final UpdateInfo updateInfo) throws Exception {
final NoteStoreClient noteStore = getNoteStoreClient(updateInfo);
final SyncState syncState = noteStore.getSyncState();
final String lastSyncTimeAtt = guestService.getApiKeyAttribute(updateInfo.apiKey, LAST_SYNC_TIME);
long lastSyncTime = Long.valueOf(lastSyncTimeAtt);
final String lastUpdateCountAtt = guestService.getApiKeyAttribute(updateInfo.apiKey, LAST_UPDATE_COUNT);
long lastUpdateCount = Long.valueOf(lastUpdateCountAtt);
if (syncState.getFullSyncBefore()>lastSyncTime) {
// according to the edam sync spec, fullSyncBefore is "the cut-off date for old caching clients
// to perform an incremental (vs. full) synchronization. This value may correspond to the point
// where historic data (e.g. regarding expunged objects) was wiped from the account, or possibly
// the time of a serious server issue that would invalidate client USNs"
// This means that we are may leave items that the user actually deleted in the database, and thus
// we need to basically do a history update again
apiDataService.eraseApiData(updateInfo.apiKey, false);
guestService.removeApiKeyAttribute(updateInfo.apiKey.getId(), LAST_SYNC_TIME);
guestService.removeApiKeyAttribute(updateInfo.apiKey.getId(), LAST_UPDATE_COUNT);
// let's properly log this
logger.info("FullSync required for evernote connector, apiKeyId=" + updateInfo.apiKey.getId());
performSync(updateInfo, noteStore, true);
}
else if (syncState.getUpdateCount()==lastUpdateCount)
// nothing happened since we last updated
return;
else
performSync(updateInfo, noteStore, false);
}
@Override
public void connectorSettingsChanged(final long apiKeyId, final Object settings) {
final EvernoteConnectorSettings connectorSettings = (EvernoteConnectorSettings)settings;
final ApiKey apiKey = guestService.getApiKey(apiKeyId);
setChannelMapping(apiKey, connectorSettings.notebooks);
}
@Override
public Object syncConnectorSettings(final UpdateInfo updateInfo, Object s) {
EvernoteConnectorSettings settings = s ==null
? new EvernoteConnectorSettings()
: (EvernoteConnectorSettings) s;
// get notebooks, add new configs for new notebooks...
final List<EvernoteNotebookFacet> notebooks = jpaDaoService.find("evernote.notebooks.byApiKeyId",
EvernoteNotebookFacet.class, updateInfo.apiKey.getId());
there: for (EvernoteNotebookFacet notebook : notebooks) {
for (NotebookConfig notebookConfig : settings.notebooks) {
if (notebookConfig.guid.equals(notebook.guid))
continue there;
}
NotebookConfig config = new NotebookConfig();
config.guid = notebook.guid;
config.name = notebook.name;
config.isDefault = notebook.defaultNotebook==null?false:notebook.defaultNotebook;
config.backgroundColor = EVERNOTE_DEFAULT_BGCOLOR;
settings.addNotebookConfig(config);
}
// and remove configs for deleted notebooks - leave others untouched
List<NotebookConfig> configsToDelete = new ArrayList<NotebookConfig>();
there: for (NotebookConfig notebookConfig : settings.notebooks) {
for (EvernoteNotebookFacet notebook : notebooks) {
if (notebookConfig.guid.equals(notebook.guid))
continue there;
}
configsToDelete.add(notebookConfig);
}
for (NotebookConfig notebookConfig : configsToDelete) {
final NotebookConfig toDelete = settings.getNotebook(notebookConfig.guid);
settings.notebooks.remove(toDelete);
}
// retrieve tags and store tag guid -> tag name map in the settings
final List<EvernoteTagFacet> tags = jpaDaoService.find("evernote.tags.byApiKeyId",
EvernoteTagFacet.class, updateInfo.apiKey.getId());
Map<String,String> tagsMap = new HashMap<String,String>();
for (EvernoteTagFacet tag : tags)
tagsMap.put(tag.guid, tag.name);
settings.tags = tagsMap;
return settings;
}
@Override
public void syncSharedConnectorSettings(final long apiKeyId, final SharedConnector sharedConnector) {
JSONObject jsonSettings = new JSONObject();
if (sharedConnector.filterJson!=null)
jsonSettings = JSONObject.fromObject(sharedConnector.filterJson);
// get notebooks, add new configs for new notebooks...
final List<EvernoteNotebookFacet> notebooks = jpaDaoService.find("evernote.notebooks.byApiKeyId",
EvernoteNotebookFacet.class, apiKeyId);
JSONArray settingsNotebooks = new JSONArray();
if (jsonSettings.has("notebooks"))
settingsNotebooks = jsonSettings.getJSONArray("notebooks");
there: for (EvernoteNotebookFacet notebook : notebooks) {
for (int i=0; i<settingsNotebooks.size(); i++) {
JSONObject notebookConfig = settingsNotebooks.getJSONObject(i);
if (notebookConfig.getString("guid").equals(notebook.guid))
continue there;
}
JSONObject config = new JSONObject();
config.accumulate("guid", notebook.guid);
config.accumulate("name", notebook.name);
config.accumulate("shared", false);
settingsNotebooks.add(config);
}
// and remove configs for deleted notebooks - leave others untouched
JSONArray settingsToDelete = new JSONArray();
there: for (int i=0; i<settingsNotebooks.size(); i++) {
JSONObject notebookConfig = settingsNotebooks.getJSONObject(i);
for (EvernoteNotebookFacet notebook : notebooks) {
if (notebookConfig.getString("guid").equals(notebook.guid))
continue there;
}
settingsToDelete.add(notebookConfig);
}
for (int i=0; i<settingsToDelete.size(); i++) {
JSONObject toDelete = settingsToDelete.getJSONObject(i);
for (int j=0; j<settingsNotebooks.size(); j++) {
if (settingsNotebooks.getJSONObject(j).getString("guid").equals(toDelete.getString("guid"))) {
settingsNotebooks.remove(j);
}
}
}
jsonSettings.put("notebooks", settingsNotebooks);
String toPersist = jsonSettings.toString();
buddiesService.setSharedConnectorFilter(sharedConnector.getId(), toPersist);
}
@Override
public void setDefaultChannelStyles(ApiKey apiKey) {
// the styles for this connector depend on the number of notebooks available, so this is empty
}
private void setChannelMapping(ApiKey apiKey, final List<NotebookConfig> notebookConfigs) {
BodyTrackHelper.ChannelStyle channelStyle = new BodyTrackHelper.ChannelStyle();
channelStyle.timespanStyles = new BodyTrackHelper.MainTimespanStyle();
channelStyle.timespanStyles.defaultStyle = new BodyTrackHelper.TimespanStyle();
channelStyle.timespanStyles.defaultStyle.fillColor = EVERNOTE_DEFAULT_BGCOLOR;
channelStyle.timespanStyles.defaultStyle.borderColor = EVERNOTE_DEFAULT_BGCOLOR;
channelStyle.timespanStyles.defaultStyle.borderWidth = 2;
channelStyle.timespanStyles.defaultStyle.top = 0.0;
channelStyle.timespanStyles.defaultStyle.bottom = 1.0;
channelStyle.timespanStyles.values = new HashMap();
addStyleParts(notebookConfigs, channelStyle);
bodyTrackHelper.deleteStyle(apiKey.getGuestId(), apiKey.getConnector().getName());
bodyTrackHelper.setDefaultStyle(apiKey.getGuestId(), apiKey.getConnector().getName(), "notes", channelStyle);
}
private int getNumberOfVisibleNotebooks(final List<NotebookConfig> notebookConfigs) {
int nNotebooks = 0;
for (NotebookConfig calendar : notebookConfigs) {
if (!calendar.hidden)
nNotebooks++;
}
return nNotebooks;
}
void addStyleParts(final List<NotebookConfig> notebookConfigs,
final BodyTrackHelper.ChannelStyle channelStyle) {
int nNotebooks = getNumberOfVisibleNotebooks(notebookConfigs);
double rowHeight = 1.f/(nNotebooks*2+1);
int i=0;
for (NotebookConfig config: notebookConfigs) {
if (config.hidden)
continue;
BodyTrackHelper.TimespanStyle stylePart = new BodyTrackHelper.TimespanStyle();
final int rowsFromTop = (i+1) * 2 - 1;
stylePart.top = (double)rowsFromTop*rowHeight-(rowHeight*0.25);
stylePart.bottom = stylePart.top+rowHeight+(rowHeight*0.25);
stylePart.fillColor = config.backgroundColor;
stylePart.borderColor = config.backgroundColor;
channelStyle.timespanStyles.values.put(config.guid, stylePart);
i++;
}
}
private NoteStoreClient getNoteStoreClient(final UpdateInfo updateInfo) throws EDAMUserException, EDAMSystemException, TException {
final Boolean sandbox = Boolean.valueOf(guestService.getApiKeyAttribute(updateInfo.apiKey, EvernoteController.EVERNOTE_SANDBOX_KEY));
String token = guestService.getApiKeyAttribute(updateInfo.apiKey, "accessToken");
EvernoteAuth evernoteAuth = new EvernoteAuth(sandbox?EvernoteService.SANDBOX:EvernoteService.PRODUCTION, token);
ClientFactory factory = new ClientFactory(evernoteAuth);
return factory.createNoteStoreClient();
}
private void performSync(final UpdateInfo updateInfo, final NoteStoreClient noteStore, final boolean fullSync) throws Exception {
try {
// retrieve lastUpdateCount - this could be an incremental update or
// a second attempt at a previously failed history update
final String lastUpdateCountAtt = guestService.getApiKeyAttribute(updateInfo.apiKey, LAST_UPDATE_COUNT);
int lastUpdateCount = 0;
if (lastUpdateCountAtt!=null)
lastUpdateCount = Integer.valueOf(lastUpdateCountAtt);
// retrieve sync chunks at once
LinkedList<SyncChunk> chunks = getSyncChunks(noteStore, lastUpdateCount, fullSync);
createOrUpdateTags(updateInfo, chunks);
createOrUpdateNotebooks(updateInfo, chunks);
createOrUpdateNotes(updateInfo, chunks, noteStore);
// process expunged items in the case of an incremental update and we are not required
// to do a full sync (in which case it would be a no-op)
if (updateInfo.getUpdateType()==UpdateInfo.UpdateType.INCREMENTAL_UPDATE && !fullSync) {
processExpungedNotes(updateInfo, chunks);
processExpungedNotebooks(updateInfo, chunks);
processExpungedTags(updateInfo, chunks);
}
saveSyncState(updateInfo, noteStore);
resetChannelMapping(updateInfo);
} catch (EDAMSystemException e) {
// if rate limit has been reached, EN will send us the time when we can call the API again
// and we can explicitely inform the userInfo object of it
if (e.getErrorCode()== EDAMErrorCode.RATE_LIMIT_REACHED) {
updateInfo.setResetTime("all", System.currentTimeMillis()+e.getRateLimitDuration()*1000);
throw new RateLimitReachedException();
}
} catch (EDAMUserException e) {
// if the auth token expired, we have no other choice than to have the user re-authenticate
if (e.getErrorCode()==EDAMErrorCode.AUTH_EXPIRED) {
throw new AuthExpiredException();
}
}
}
private void processExpungedNotes(final UpdateInfo updateInfo, final LinkedList<SyncChunk> chunks) {
List<String> expungedNoteGuids = new ArrayList<String>();
for (SyncChunk chunk : chunks) {
final List<String> chunkExpungedNotes = chunk.getExpungedNotes();
if (chunkExpungedNotes!=null)
expungedNoteGuids.addAll(chunkExpungedNotes);
}
for (String expungedNoteGuid : expungedNoteGuids) {
removeNote(updateInfo, expungedNoteGuid);
}
}
/**
* Delete a note, its associated resources and their dependent files from permanent storage
* @param updateInfo
* @param noteGuid
*/
private void removeNote(final UpdateInfo updateInfo, final String noteGuid) {
// first remove the note itself
removeEvernoteFacet(updateInfo, EvernoteNoteFacet.class, noteGuid);
// now retrieve the info needed to figure out what its associated resources and their dependent files are
final List resourceInfos = jpaDaoService.executeNativeQuery(String.format("SELECT guid, mime FROM %s facet WHERE facet.apiKeyId=(?1) AND facet.noteGuid=(?2)",
JPAUtils.getEntityName(EvernoteResourceFacet.class)),
updateInfo.apiKey.getId(), noteGuid);
final String devKvsLocation = env.get("btdatastore.db.location");
for (Object infos : resourceInfos) {
Object[] guidAndMime = (Object[]) infos;
String guid = (String)guidAndMime[0];
String mime = (String)guidAndMime[1];
// if the resource is a photo, remove its associated photo facet
removeEvernoteFacet(updateInfo, EvernotePhotoFacet.class, guid);
// remove the resource from the database
removeEvernoteFacet(updateInfo, EvernoteResourceFacet.class, guid);
// now retrieve the associated data files and delete them if they exist
final File resourceDataFile = getResourceFile(updateInfo.getGuestId(), updateInfo.apiKey.getId(), guid, MAIN_APPENDIX, mime, devKvsLocation);
final File resourceAlternateDataFile = getResourceFile(updateInfo.getGuestId(), updateInfo.apiKey.getId(), guid, ALTERNATE_APPENDIX, mime, devKvsLocation);
final File resourceRecognitionDataFile = getResourceFile(updateInfo.getGuestId(), updateInfo.apiKey.getId(), guid, RECOGNITION_APPENDIX, mime, devKvsLocation);
if (resourceDataFile.exists())
resourceDataFile.delete();
if (resourceAlternateDataFile.exists())
resourceAlternateDataFile.delete();
if (resourceRecognitionDataFile.exists())
resourceRecognitionDataFile.delete();
}
removeLocationFacets(updateInfo.apiKey.getId(), noteGuid);
}
private void removeLocationFacets(final long apiKeyId, final String noteGuid) {
final int locationsDeleted =
jpaDaoService.execute(String.format("DELETE FROM %s facet WHERE facet.apiKeyId=? AND facet.uri=?",
JPAUtils.getEntityName(LocationFacet.class)),
apiKeyId, noteGuid);
System.out.println(locationsDeleted + " note locations were deleted");
}
private void processExpungedNotebooks(final UpdateInfo updateInfo, final LinkedList<SyncChunk> chunks) {
List<String> expungedNotebookGuids = new ArrayList<String>();
for (SyncChunk chunk : chunks) {
final List<String> chunkExpungedNotebooks = chunk.getExpungedNotebooks();
if (chunkExpungedNotebooks!=null)
expungedNotebookGuids.addAll(chunkExpungedNotebooks);
}
for (String expungedNotebookGuid : expungedNotebookGuids)
removeNotebook(updateInfo, expungedNotebookGuid);
}
private void processExpungedTags(final UpdateInfo updateInfo, final LinkedList<SyncChunk> chunks) {
List<String> expungedTagGuids = new ArrayList<String>();
for (SyncChunk chunk : chunks) {
final List<String> chunkExpungedTags = chunk.getExpungedTags();
if (chunkExpungedTags!=null)
expungedTagGuids.addAll(chunkExpungedTags);
}
for (String expungedTagGuid : expungedTagGuids)
removeEvernoteFacet(updateInfo, EvernoteTagFacet.class, expungedTagGuid);
}
private void createOrUpdateTags(final UpdateInfo updateInfo, final LinkedList<SyncChunk> chunks) throws Exception {
List<Tag> tags = new ArrayList<Tag>();
List<String> expungedTagGuids = new ArrayList<String>();
for (SyncChunk chunk : chunks){
final List<Tag> chunkTags = chunk.getTags();
if (chunkTags!=null)
tags.addAll(chunkTags);
final List<String> expungedTags = chunk.getExpungedTags();
if (expungedTags!=null)
expungedTagGuids.addAll(expungedTags);
}
for (Tag tag : tags) {
if (!expungedTagGuids.contains(tag.getGuid()))
createOrUpdateTag(updateInfo, tag);
}
}
private void createOrUpdateNotebooks(final UpdateInfo updateInfo, final LinkedList<SyncChunk> chunks) throws Exception {
List<Notebook> notebooks = new ArrayList<Notebook>();
List<String> expungedNotebookGuids = new ArrayList<String>();
for (SyncChunk chunk : chunks){
final List<Notebook> chunkNotebooks = chunk.getNotebooks();
if (chunkNotebooks!=null)
notebooks.addAll(chunkNotebooks);
final List<String> expungedNotebooks = chunk.getExpungedNotebooks();
if (expungedNotebooks!=null)
expungedNotebookGuids.addAll(expungedNotebooks);
}
for (Notebook notebook : notebooks) {
if (!expungedNotebookGuids.contains(notebook.getGuid()))
createOrUpdateNotebook(updateInfo, notebook);
}
}
private void createOrUpdateNotes(final UpdateInfo updateInfo, final LinkedList<SyncChunk> chunks,
NoteStoreClient noteStore) throws Exception {
List<Note> notes = new ArrayList<Note>();
List<String> expungedNoteGuids = new ArrayList<String>();
for (SyncChunk chunk : chunks){
final List<Note> chunkNotes = chunk.getNotes();
if (chunkNotes!=null)
notes.addAll(chunkNotes);
final List<String> chunkExpungedNotes = chunk.getExpungedNotes();
if (chunkExpungedNotes!=null)
expungedNoteGuids.addAll(chunkExpungedNotes);
}
for (Note note : notes) {
if (!expungedNoteGuids.contains(note.getGuid()))
createOrUpdateNote(updateInfo, note, noteStore);
}
}
private LinkedList<SyncChunk> getSyncChunks(final NoteStoreClient noteStore, final int lastUpdateCount, final boolean fullSync) throws EDAMUserException, EDAMSystemException, TException {
LinkedList<SyncChunk> chunks = new LinkedList<SyncChunk>();
SyncChunk chunk = noteStore.getSyncChunk(lastUpdateCount, MAX_ENTRIES, fullSync);
if (chunk!=null) {
chunks.add(chunk);
while (chunk.getChunkHighUSN()<chunk.getUpdateCount()) {
chunk = noteStore.getSyncChunk(chunk.getChunkHighUSN(), MAX_ENTRIES, fullSync);
if (chunk!=null)
chunks.add(chunk);
}
}
return chunks;
}
private void saveSyncState(final UpdateInfo updateInfo, NoteStoreClient noteStore) throws TException, EDAMUserException, EDAMSystemException {
final SyncState syncState = noteStore.getSyncState();
int serviceLastUpdateCount = syncState.getUpdateCount();
final long serviceLastSyncTime = syncState.getCurrentTime();
final String lastUpdateCountAtt = String.valueOf(serviceLastUpdateCount);
guestService.setApiKeyAttribute(updateInfo.apiKey, LAST_UPDATE_COUNT, lastUpdateCountAtt);
final String lastSyncTimeAtt = String.valueOf(serviceLastSyncTime);
guestService.setApiKeyAttribute(updateInfo.apiKey, LAST_SYNC_TIME, lastSyncTimeAtt);
}
private void createOrUpdateNote(final UpdateInfo updateInfo, final Note note, final NoteStoreClient noteStore) throws Exception {
final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery(
"e.apiKeyId=? AND e.guid=?",
updateInfo.apiKey.getId(), note.getGuid());
final ApiDataService.FacetModifier<EvernoteNoteFacet> facetModifier = new ApiDataService.FacetModifier<EvernoteNoteFacet>() {
@Override
public EvernoteNoteFacet createOrModify(EvernoteNoteFacet facet, final Long apiKeyId) throws Exception {
if (facet == null) {
facet = new EvernoteNoteFacet(updateInfo.apiKey.getId());
extractCommonFacetData(facet, updateInfo);
facet.guid = note.getGuid();
}
if ( facet.USN==null
|| facet.USN<note.getUpdateSequenceNum()
|| facet.contentHash==null
|| !Arrays.equals(facet.contentHash, note.getContentHash())
|| facet.contentLength!=note.getContentLength())
{
Note freshlyRetrievedNote = noteStore.getNote(note.getGuid(), true, true, true, true);
facet.timeUpdated = System.currentTimeMillis();
if (freshlyRetrievedNote.isSetUpdateSequenceNum())
facet.USN = freshlyRetrievedNote.getUpdateSequenceNum();
if (freshlyRetrievedNote.isSetContentHash())
facet.contentHash = freshlyRetrievedNote.getContentHash();
if (freshlyRetrievedNote.isSetContentLength())
facet.contentLength = freshlyRetrievedNote.getContentLength();
Map<String, String> mapHashtoURL = new HashMap<String, String>();
if (freshlyRetrievedNote.isSetCreated()) {
facet.created = freshlyRetrievedNote.getCreated();
facet.start = facet.created;
facet.end = facet.created;
}
if (freshlyRetrievedNote.isSetUpdated()) {
facet.updated = freshlyRetrievedNote.getUpdated();
facet.start = facet.updated;
facet.end = facet.updated;
}
if (freshlyRetrievedNote.isSetNotebookGuid())
facet.notebookGuid = freshlyRetrievedNote.getNotebookGuid();
if (freshlyRetrievedNote.isSetResources()) {
for (Resource resource : freshlyRetrievedNote.getResources()) {
createOrUpdateResource(updateInfo, resource);
// save the resource a second time as a photo -
// the facet will hold a reference to the original resource facet
if (resource.isSetAttributes()&&resource.getAttributes().isSetCameraMake())
createOrUpdatePhoto(updateInfo, resource, facet.start);
String webResourcePath = new StringBuilder("/evernote/res/")
.append(updateInfo.apiKey.getId())
.append("/")
.append(resource.getGuid()).toString();
mapHashtoURL.put(resource.getGuid(), webResourcePath);
}
}
if (freshlyRetrievedNote.isSetContent()) {
facet.content = freshlyRetrievedNote.getContent();
// WARNING!! The first time this gets called, a lengthy DTD processing operation
// needs to happen which can take a long while (~1min) - after that the conversion
// from enml to xhtml is very fast
try {
final String htmlContent = processor.noteToHTMLString(freshlyRetrievedNote, mapHashtoURL);
facet.htmlContent = htmlContent;
} catch (Throwable t) {
logger.warn("error parsing enml note: " + t.getMessage());
System.out.println(ExceptionUtils.getStackTrace(t));
facet.htmlContent = "Sorry, there was an error parsing this note (" + t.getMessage() +")";
}
}
facet.clearTags();
if (freshlyRetrievedNote.isSetTagNames()) {
final List<String> tagNames = freshlyRetrievedNote.getTagNames();
facet.addTags(StringUtils.join(tagNames, ","), ',');
}
if (freshlyRetrievedNote.isSetTagGuids()) {
final List<String> tagGuids = freshlyRetrievedNote.getTagGuids();
facet.setTagGuids(tagGuids);
}
if (freshlyRetrievedNote.isSetTitle())
facet.title = freshlyRetrievedNote.getTitle();
if (freshlyRetrievedNote.isSetActive())
facet.active = freshlyRetrievedNote.isActive();
if (freshlyRetrievedNote.isSetAttributes()) {
final NoteAttributes attributes = freshlyRetrievedNote.getAttributes();
if (attributes.isSetAltitude())
facet.altitude = attributes.getAltitude();
if (attributes.isSetAuthor())
facet.author = attributes.getAuthor();
if (attributes.isSetContentClass())
facet.contentClass = attributes.getContentClass();
if (attributes.isSetCreatorId())
facet.creatorId = attributes.getCreatorId();
if (attributes.isSetLastEditedBy())
facet.lastEditedBy = attributes.getLastEditedBy();
if (attributes.isSetLastEditorId())
facet.lastEditorId = attributes.getLastEditorId();
if (attributes.isSetLatitude())
facet.latitude = attributes.getLatitude();
if (attributes.isSetLongitude())
facet.longitude = attributes.getLongitude();
if (attributes.isSetPlaceName())
facet.placeName = attributes.getPlaceName();
if (attributes.isSetReminderDoneTime())
facet.reminderDoneTime = attributes.getReminderDoneTime();
if (attributes.isSetReminderOrder())
facet.reminderOrder = attributes.getReminderOrder();
if (attributes.isSetReminderTime())
facet.reminderTime = attributes.getReminderTime();
if (attributes.isSetShareDate())
facet.shareDate = attributes.getShareDate();
if (attributes.isSetSource())
facet.source = attributes.getSource();
if (attributes.isSetSourceApplication())
facet.sourceApplication = attributes.getSourceApplication();
if (attributes.isSetSourceURL())
facet.sourceURL = attributes.getSourceURL();
if (attributes.isSetSubjectDate())
facet.subjectDate = attributes.getSubjectDate();
if (attributes.isSetLatitude()&&attributes.isSetLongitude()&&freshlyRetrievedNote.isSetCreated()){
addGuestLocation(updateInfo, facet.latitude, facet.longitude, facet.altitude, facet.created, facet.guid);
}
}
if (freshlyRetrievedNote.isSetDeleted()) {
facet.deleted = freshlyRetrievedNote.getDeleted();
// if the note was deleted:
// remove locations that were attached to this note and its associated resources
if (freshlyRetrievedNote.isSetGuid())
removeLocationFacets(updateInfo.apiKey.getId(), freshlyRetrievedNote.getGuid());
} else if (!freshlyRetrievedNote.isSetDeleted()&&facet.deleted!=null) {
facet.deleted = null;
// this means that this note was restored from trash and we need to restore
// its associated resources' locations
if (freshlyRetrievedNote.isSetGuid())
restoreNoteResourceLocations(updateInfo, freshlyRetrievedNote.getGuid());
}
}
return facet;
}
};
// we could use the resulting value (facet) from this call if we needed to do further processing on it (e.g. passing it on to the datastore)
apiDataService.createOrReadModifyWrite(EvernoteNoteFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId());
}
private void restoreNoteResourceLocations(final UpdateInfo updateInfo, final String noteGuid) {
final List<EvernoteResourceFacet> evernoteResourceFacets =
jpaDaoService.find("evernote.resources.byApiKeyIdAndNoteGuid", EvernoteResourceFacet.class, updateInfo.apiKey.getId(), noteGuid);
for (EvernoteResourceFacet evernoteResourceFacet : evernoteResourceFacets) {
if (evernoteResourceFacet.latitude!=null && evernoteResourceFacet.longitude!=null) {
addGuestLocation(updateInfo, evernoteResourceFacet.latitude, evernoteResourceFacet.longitude,
evernoteResourceFacet.altitude, evernoteResourceFacet.start, noteGuid);
}
}
}
private void createOrUpdatePhoto(final UpdateInfo updateInfo, final Resource resource, final long start) throws Exception {
final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery(
"e.apiKeyId=? AND e.guid=?",
updateInfo.apiKey.getId(), resource.getGuid());
final ApiDataService.FacetModifier<EvernotePhotoFacet> facetModifier = new ApiDataService.FacetModifier<EvernotePhotoFacet>() {
@Override
public EvernotePhotoFacet createOrModify(EvernotePhotoFacet facet, final Long apiKeyId) throws Exception {
if (facet == null) {
facet = new EvernotePhotoFacet(updateInfo.apiKey.getId());
extractCommonFacetData(facet, updateInfo);
facet.guid = resource.getGuid();
}
facet.start = start;
facet.end = start;
final List<EvernoteResourceFacet> resourceFacets =
jpaDaoService.executeQueryWithLimit(String.format("SELECT facet from %s facet WHERE facet.apiKeyId=? AND facet.guid=?",
JPAUtils.getEntityName(EvernoteResourceFacet.class)),
1, EvernoteResourceFacet.class, apiKeyId, resource.getGuid());
// in theory this list should always be non-empty but we don't want to risk crashing at this point
if (resourceFacets.size()>0)
facet.resourceFacet = resourceFacets.get(0);
else
return null;
// now all the useful information i s in the resource facet, really
return facet;
}
};
apiDataService.createOrReadModifyWrite(EvernotePhotoFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId());
}
private void createOrUpdateResource(final UpdateInfo updateInfo, final Resource resource) throws Exception {
final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery(
"e.apiKeyId=? AND e.guid=?",
updateInfo.apiKey.getId(), resource.getGuid());
final ApiDataService.FacetModifier<EvernoteResourceFacet> facetModifier = new ApiDataService.FacetModifier<EvernoteResourceFacet>() {
@Override
public EvernoteResourceFacet createOrModify(EvernoteResourceFacet facet, final Long apiKeyId) throws Exception {
if (facet == null) {
facet = new EvernoteResourceFacet(updateInfo.apiKey.getId());
extractCommonFacetData(facet, updateInfo);
facet.guid = resource.getGuid();
}
if (facet.USN==null||facet.USN<resource.getUpdateSequenceNum()) {
if (resource.isSetAlternateData()) {
Data alternateData = resource.getAlternateData();
if (alternateData.isSetBody()&&resource.isSetGuid())
saveDataBodyAsFile(updateInfo, resource.getGuid(), ALTERNATE_APPENDIX, alternateData.getBody(), resource.getMime());
if (alternateData.isSetBodyHash())
facet.alternateDataBodyHash = alternateData.getBodyHash();
if (alternateData.isSetSize())
facet.alternateDataSize = alternateData.getSize();
}
if (resource.isSetAttributes()) {
final ResourceAttributes resourceAttributes = resource.getAttributes();
if (resourceAttributes.isSetAltitude())
facet.altitude = resourceAttributes.getAltitude();
if (resourceAttributes.isSetAttachment())
facet.isAttachment = resourceAttributes.isAttachment();
if (resourceAttributes.isSetCameraMake())
facet.cameraMake = resourceAttributes.getCameraMake();
if (resourceAttributes.isSetCameraModel())
facet.cameraModel = resourceAttributes.getCameraModel();
if (resourceAttributes.isSetFileName())
facet.fileName = resourceAttributes.getFileName();
if (resourceAttributes.isSetLatitude())
facet.latitude = resourceAttributes.getLatitude();
if (resourceAttributes.isSetLongitude())
facet.longitude = resourceAttributes.getLongitude();
if (resourceAttributes.isSetRecoType())
facet.recoType = resourceAttributes.getRecoType();
if (resourceAttributes.isSetSourceURL())
facet.sourceURL = resourceAttributes.getSourceURL();
if (resourceAttributes.isSetTimestamp())
facet.timestamp = resourceAttributes.getTimestamp();
if (resourceAttributes.isSetTimestamp() &&
resourceAttributes.isSetLongitude() &&
resourceAttributes.isSetLatitude()&&
resource.isSetNoteGuid()){
// resource locations are associated with their parent note's guid
addGuestLocation(updateInfo, facet.latitude, facet.longitude, facet.altitude,
facet.timestamp, resource.getNoteGuid());
}
}
if (resource.isSetData()) {
Data data = resource.getData();
if (data.isSetBody()&&resource.isSetGuid())
saveDataBodyAsFile(updateInfo, resource.getGuid(), MAIN_APPENDIX, data.getBody(), resource.getMime());
if (data.isSetBodyHash())
facet.dataBodyHash = data.getBodyHash();
if (data.isSetSize())
facet.dataSize = data.getSize();
}
if (resource.isSetHeight())
facet.height = resource.getHeight();
if (resource.isSetMime())
facet.mime = resource.getMime();
if (resource.isSetNoteGuid())
facet.noteGuid = resource.getNoteGuid();
if (resource.isSetRecognition()) {
Data recognitionData = resource.getRecognition();
if (recognitionData.isSetBody()&&resource.isSetGuid())
saveDataBodyAsFile(updateInfo, resource.getGuid(), RECOGNITION_APPENDIX, recognitionData.getBody(), null);
if (recognitionData.isSetBodyHash())
facet.recognitionDataBodyHash = recognitionData.getBodyHash();
if (recognitionData.isSetSize())
facet.recognitionDataSize = recognitionData.getSize();
}
if (resource.isSetUpdateSequenceNum())
facet.USN = resource.getUpdateSequenceNum();
if (resource.isSetWidth())
facet.width = resource.getWidth();
}
return facet;
}
};
apiDataService.createOrReadModifyWrite(EvernoteResourceFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId());
}
private void saveDataBodyAsFile(final UpdateInfo updateInfo, final String guid, final String appendix, final byte[] body, final String mimeType) throws IOException {
final String devKvsLocation = env.get("btdatastore.db.location");
File file = getResourceFile(updateInfo.getGuestId(), updateInfo.apiKey.getId(), guid, appendix, mimeType, devKvsLocation);
file.getParentFile().mkdirs();
FileOutputStream fileoutput = new FileOutputStream(file);
IOUtils.copy(new ByteArrayInputStream(body), fileoutput);
fileoutput.close();
}
/**
*
* @param apiKeyId
* @param guid
* @param appendix
* @param mimeType
* @param devKvsLocation
* @return
*/
public static File getResourceFile(final long guestId, final long apiKeyId,
final String guid, final String appendix,
final String mimeType, final String devKvsLocation) {
String extension = getFileExtension(mimeType);
if (appendix.equals(RECOGNITION_APPENDIX))
extension = ".xml";
return new File(new StringBuilder(devKvsLocation).append(File.separator)
.append(guestId)
.append(File.separator)
.append(Connector.getConnector("evernote").prettyName())
.append(File.separator)
.append(apiKeyId)
.append(File.separator)
.append(guid)
.append(appendix.equals(MAIN_APPENDIX) ? "" : "_")
.append(appendix.equals(MAIN_APPENDIX) ? "" : appendix)
.append(extension).toString());
}
private static String getFileExtension(String mimeType) {
if (StringUtils.isEmpty(mimeType)) return "";
MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
try {
MimeType type = allTypes.forName(mimeType);
return type.getExtension();
}
catch (MimeTypeException e) {
return "";
}
}
private void addGuestLocation(final UpdateInfo updateInfo, final Double latitude, final Double longitude,
final Double altitude, final Long timestamp, final String noteGuid) {
LocationFacet locationFacet = new LocationFacet(updateInfo.apiKey.getId());
locationFacet.latitude = latitude.floatValue();
locationFacet.longitude = longitude.floatValue();
if (altitude!=null)
locationFacet.altitude = altitude.intValue();
locationFacet.timestampMs = timestamp;
locationFacet.start = locationFacet.timestampMs;
locationFacet.end = locationFacet.timestampMs;
locationFacet.source = LocationFacet.Source.EVERNOTE;
locationFacet.apiKeyId = updateInfo.apiKey.getId();
locationFacet.api = connector().value();
locationFacet.uri = noteGuid;
apiDataService.addGuestLocation(updateInfo.getGuestId(), locationFacet);
}
private void removeEvernoteFacet(final UpdateInfo updateInfo, Class<? extends EvernoteFacet> clazz, final String guid) {
jpaDaoService.execute(String.format("DELETE FROM %s facet WHERE facet.apiKeyId=%s AND facet.guid='%s'", JPAUtils.getEntityName(clazz), updateInfo.apiKey.getId(), guid));
}
private void createOrUpdateNotebook(final UpdateInfo updateInfo, final Notebook notebook) throws Exception {
final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery(
"e.apiKeyId=? AND e.guid=?",
updateInfo.apiKey.getId(), notebook.getGuid());
final ApiDataService.FacetModifier<EvernoteNotebookFacet> facetModifier = new ApiDataService.FacetModifier<EvernoteNotebookFacet>() {
@Override
public EvernoteNotebookFacet createOrModify(EvernoteNotebookFacet facet, final Long apiKeyId) throws Exception {
if (facet == null) {
facet = new EvernoteNotebookFacet(updateInfo.apiKey.getId());
extractCommonFacetData(facet, updateInfo);
facet.guid = notebook.getGuid();
}
facet.timeUpdated = System.currentTimeMillis();
if (notebook.isSetUpdateSequenceNum())
facet.USN = notebook.getUpdateSequenceNum();
if (notebook.isSetName())
facet.name = notebook.getName();
if (notebook.isSetServiceCreated()) {
facet.serviceCreated = notebook.getServiceCreated();
}
if (notebook.isSetServiceUpdated())
facet.serviceUpdated = notebook.getServiceUpdated();
if (notebook.isSetPublishing()) {
final Publishing publishing = notebook.getPublishing();
if (publishing.isSetOrder()) {
final NoteSortOrder order = publishing.getOrder();
facet.publishingNoteOrderValue = order.getValue();
}
if (publishing.isSetUri())
facet.publishingUri = publishing.getUri();
if (publishing.isSetPublicDescription())
facet.publishingPublicDescription = publishing.getPublicDescription();
}
if (notebook.isSetPublished())
facet.published = notebook.isPublished();
if (notebook.isSetStack())
facet.stack = notebook.getStack();
if (notebook.isSetDefaultNotebook())
facet.defaultNotebook = notebook.isDefaultNotebook();
//omitting business information: contact, businessNotebook
//omitting sharedNotebooks
return facet;
}
};
// we could use the resulting value (facet) from this call if we needed to do further processing on it (e.g. passing it on to the datastore)
apiDataService.createOrReadModifyWrite(EvernoteNotebookFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId());
}
private void removeNotebook(final UpdateInfo updateInfo, final String guid) {
// remove the notebook itself
removeEvernoteFacet(updateInfo, EvernoteNotebookFacet.class, guid);
// now retrieve the guids of all the notes it contains so we can wipe them out along
// with the resources they reference
// Note: it is possible that these notes are part of the expunged notes list in
// the SyncChunk already, so this might be unnecessary
final List noteGuids = jpaDaoService.executeNativeQuery(
String.format("SELECT guid FROM %s facet WHERE facet.apiKeyId=(?1) AND facet.notebookGuid=(?2)",
JPAUtils.getEntityName(EvernoteNoteFacet.class)),
updateInfo.apiKey.getId(), guid);
for (Object noteGuid : noteGuids) {
// not sure if we are getting back an array of objects or just a string, let's test for both
if (noteGuid instanceof String)
removeNote(updateInfo, (String) noteGuid);
else if (noteGuid instanceof Object[])
removeNote(updateInfo, (String) ((Object[])noteGuid)[0]);
}
}
private void createOrUpdateTag(final UpdateInfo updateInfo, final Tag tag) throws Exception {
final ApiDataService.FacetQuery facetQuery = new ApiDataService.FacetQuery(
"e.apiKeyId=? AND e.guid=?",
updateInfo.apiKey.getId(), tag.getGuid());
final ApiDataService.FacetModifier<EvernoteTagFacet> facetModifier = new ApiDataService.FacetModifier<EvernoteTagFacet>() {
@Override
public EvernoteTagFacet createOrModify(EvernoteTagFacet facet, final Long apiKeyId) {
if (facet == null) {
facet = new EvernoteTagFacet(updateInfo.apiKey.getId());
extractCommonFacetData(facet, updateInfo);
facet.guid = tag.getGuid();
}
facet.timeUpdated = System.currentTimeMillis();
if (tag.isSetUpdateSequenceNum())
facet.USN = tag.getUpdateSequenceNum();
if (tag.isSetName())
facet.name = tag.getName();
return facet;
}
};
// we could use the resulting value (facet) from this call if we needed to do further processing on it (e.g. passing it on to the datastore)
apiDataService.createOrReadModifyWrite(EvernoteTagFacet.class, facetQuery, facetModifier, updateInfo.apiKey.getId());
}
}