package com.automattic.simplenote.models; import android.content.Context; import android.text.TextUtils; import com.automattic.simplenote.R; import com.simperium.client.Bucket; import com.simperium.client.BucketObject; import com.simperium.client.BucketSchema; import com.simperium.client.Query; import com.simperium.client.Query.ComparisonType; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.Locale; public class Note extends BucketObject { public static final String BUCKET_NAME = "note"; public static final String MARKDOWN_TAG = "markdown"; public static final String PINNED_TAG = "pinned"; public static final String PUBLISHED_TAG = "published"; public static final String NEW_LINE = "\n"; public static final String CONTENT_PROPERTY = "content"; public static final String TAGS_PROPERTY = "tags"; public static final String SYSTEM_TAGS_PROPERTY = "systemTags"; public static final String CREATION_DATE_PROPERTY = "creationDate"; public static final String MODIFICATION_DATE_PROPERTY = "modificationDate"; public static final String SHARE_URL_PROPERTY = "shareURL"; public static final String PUBLISH_URL_PROPERTY = "publishURL"; public static final String DELETED_PROPERTY = "deleted"; public static final String TITLE_INDEX_NAME = "title"; public static final String CONTENT_PREVIEW_INDEX_NAME = "contentPreview"; public static final String PINNED_INDEX_NAME = "pinned"; public static final String MODIFIED_INDEX_NAME = "modified"; public static final String CREATED_INDEX_NAME = "created"; public static final String MATCHED_TITLE_INDEX_NAME = "matchedTitle"; public static final String MATCHED_CONTENT_INDEX_NAME = "matchedContent"; public static final String PUBLISH_URL = "http://simp.ly/publish/"; static public final String[] FULL_TEXT_INDEXES = new String[]{ Note.TITLE_INDEX_NAME, Note.CONTENT_PROPERTY}; private static final String BLANK_CONTENT = ""; private static final String SPACE = " "; private static final int MAX_PREVIEW_CHARS = 300; protected String mTitle = null; protected String mContentPreview = null; public Note(String key) { super(key, new JSONObject()); } public Note(String key, JSONObject properties) { super(key, properties); } public static Query<Note> all(Bucket<Note> noteBucket) { return noteBucket.query() .where(DELETED_PROPERTY, ComparisonType.NOT_EQUAL_TO, true); } public static Query<Note> allDeleted(Bucket<Note> noteBucket) { return noteBucket.query() .where(DELETED_PROPERTY, ComparisonType.EQUAL_TO, true); } public static Query<Note> search(Bucket<Note> noteBucket, String searchString) { return noteBucket.query() .where(DELETED_PROPERTY, ComparisonType.NOT_EQUAL_TO, true) .where(CONTENT_PROPERTY, ComparisonType.LIKE, "%" + searchString + "%"); } public static Query<Note> allInTag(Bucket<Note> noteBucket, String tag) { return noteBucket.query() .where(DELETED_PROPERTY, ComparisonType.NOT_EQUAL_TO, true) .where(TAGS_PROPERTY, ComparisonType.LIKE, tag); } @SuppressWarnings("unused") public static String dateString(Number time, boolean useShortFormat, Context context) { Calendar c = numberToDate(time); return dateString(c, useShortFormat, context); } public static String dateString(Calendar c, boolean useShortFormat, Context context) { int year, month, day; String time, date, retVal; Calendar diff = Calendar.getInstance(); diff.setTimeInMillis(diff.getTimeInMillis() - c.getTimeInMillis()); year = diff.get(Calendar.YEAR); month = diff.get(Calendar.MONTH); day = diff.get(Calendar.DAY_OF_MONTH); diff.setTimeInMillis(0); // starting time time = DateFormat.getTimeInstance(DateFormat.SHORT).format(c.getTime()); if ((year == diff.get(Calendar.YEAR)) && (month == diff.get(Calendar.MONTH)) && (day == diff.get(Calendar.DAY_OF_MONTH))) { date = context.getResources().getString(R.string.today); if (useShortFormat) retVal = time; else retVal = date + ", " + time; } else if ((year == diff.get(Calendar.YEAR)) && (month == diff.get(Calendar.MONTH)) && (day == 1)) { date = context.getResources().getString(R.string.yesterday); if (useShortFormat) retVal = date; else retVal = date + ", " + time; } else { date = new SimpleDateFormat("MMM dd", Locale.US).format(c.getTime()); retVal = date + ", " + time; } return retVal; } public static Calendar numberToDate(Number time) { Calendar date = Calendar.getInstance(); if (time != null) { // Flick Note uses millisecond resolution timestamps Simplenote expects seconds // since we only deal with create and modify timestamps, they should all have occured // at the present time or in the past. float now = date.getTimeInMillis() / 1000; float magnitude = time.floatValue() / now; if (magnitude >= 2.f) time = time.longValue() / 1000; date.setTimeInMillis(time.longValue() * 1000); } return date; } protected void updateTitleAndPreview() { // try to build a title and preview property out of content String content = getContent().trim(); if (content.length() > MAX_PREVIEW_CHARS) { content = content.substring(0, MAX_PREVIEW_CHARS - 1); } int firstNewLinePosition = content.indexOf(NEW_LINE); if (firstNewLinePosition > -1 && firstNewLinePosition < 200) { mTitle = content.substring(0, firstNewLinePosition).trim(); if (firstNewLinePosition < content.length()) { mContentPreview = content.substring(firstNewLinePosition, content.length()); mContentPreview = mContentPreview.replace(NEW_LINE, SPACE).replace(SPACE + SPACE, SPACE).trim(); } else { mContentPreview = content; } } else { mTitle = content; mContentPreview = content; } } public String getTitle() { if (mTitle == null) { updateTitleAndPreview(); } return mTitle; } public String getContent() { Object content = getProperty(CONTENT_PROPERTY); if (content == null) { return BLANK_CONTENT; } return (String) content; } public void setContent(String content) { mTitle = null; mContentPreview = null; setProperty(CONTENT_PROPERTY, content); } public String getContentPreview() { if (mContentPreview == null) { updateTitleAndPreview(); } return mContentPreview; } public Calendar getCreationDate() { return numberToDate((Number) getProperty(CREATION_DATE_PROPERTY)); } public void setCreationDate(Calendar creationDate) { setProperty(CREATION_DATE_PROPERTY, creationDate.getTimeInMillis() / 1000); } public Calendar getModificationDate() { return numberToDate((Number) getProperty(MODIFICATION_DATE_PROPERTY)); } public void setModificationDate(Calendar modificationDate) { setProperty(MODIFICATION_DATE_PROPERTY, modificationDate.getTimeInMillis() / 1000); } public String getPublishedUrl() { String urlCode = (String) getProperty(PUBLISH_URL_PROPERTY); if (TextUtils.isEmpty(urlCode)) { return ""; } return PUBLISH_URL + urlCode; } public boolean hasTag(String tag) { List<String> tags = getTags(); String tagLower = tag.toLowerCase(); for (String tagName : tags) { if (tagLower.equals(tagName.toLowerCase())) return true; } return false; } public boolean hasTag(Tag tag) { return hasTag(tag.getSimperiumKey()); } public List<String> getTags() { JSONArray tags = (JSONArray) getProperty(TAGS_PROPERTY); if (tags == null) { tags = new JSONArray(); setProperty(TAGS_PROPERTY, tags); } int length = tags.length(); List<String> tagList = new ArrayList<>(length); if (length == 0) return tagList; for (int i = 0; i < length; i++) { String tag = tags.optString(i); if (!tag.equals("")) tagList.add(tag); } return tagList; } public void setTags(List<String> tags) { setProperty(TAGS_PROPERTY, new JSONArray(tags)); } /** * String of tags delimited by a space */ public CharSequence getTagString() { StringBuilder tagString = new StringBuilder(); List<String> tags = getTags(); for (String tag : tags) { if (tagString.length() > 0) { tagString.append(SPACE); } tagString.append(tag); } return tagString; } /** * Sets the note's tags by providing it with a {@link String} of space * seperated tags. Filters out duplicate tags. * * @param tagString a space delimited list of tags */ public void setTagString(String tagString) { List<String> tags = getTags(); tags.clear(); if (tagString == null) { setTags(tags); return; } // Make sure string has a trailing space if (tagString.length() > 1 && !tagString.substring(tagString.length() - 1).equals(SPACE)) tagString = tagString + SPACE; // for comparing case-insensitive strings, would like to find a way to // do this without allocating a new list and strings List<String> tagsUpperCase = new ArrayList<>(); // remove all current tags int start = 0; int next; String possible; String possibleUpperCase; // search tag string for space characters and pull out individual tags do { next = tagString.indexOf(SPACE, start); if (next > start) { possible = tagString.substring(start, next); possibleUpperCase = possible.toUpperCase(); if (!possible.equals(SPACE) && !tagsUpperCase.contains(possibleUpperCase)) { tagsUpperCase.add(possibleUpperCase); tags.add(possible); } } start = next + 1; } while (next > -1); setTags(tags); } public JSONArray getSystemTags() { JSONArray tags = (JSONArray) getProperty(SYSTEM_TAGS_PROPERTY); if (tags == null) { tags = new JSONArray(); setProperty(SYSTEM_TAGS_PROPERTY, tags); } return tags; } public Boolean isDeleted() { Object deleted = getProperty(DELETED_PROPERTY); if (deleted == null) { return false; } if (deleted instanceof Boolean) { return (Boolean) deleted; } else return deleted instanceof Number && ((Number) deleted).intValue() != 0; } public void setDeleted(boolean deleted) { setProperty(DELETED_PROPERTY, deleted); } public boolean isMarkdownEnabled() { return hasSystemTag(MARKDOWN_TAG); } public void setMarkdownEnabled(boolean isMarkdownEnabled) { if (isMarkdownEnabled) { addSystemTag(MARKDOWN_TAG); } else { removeSystemTag(MARKDOWN_TAG); } } public boolean isPinned() { return hasSystemTag(PINNED_TAG); } public void setPinned(boolean isPinned) { if (isPinned) { addSystemTag(PINNED_TAG); } else { removeSystemTag(PINNED_TAG); } } public boolean isPublished() { return hasSystemTag(PUBLISHED_TAG) && !TextUtils.isEmpty(getPublishedUrl()); } public void setPublished(boolean isPublished) { if (isPublished) { addSystemTag(PUBLISHED_TAG); } else { removeSystemTag(PUBLISHED_TAG); } } private boolean hasSystemTag(String tag) { if (TextUtils.isEmpty(tag)) return false; JSONArray tags = getSystemTags(); int length = tags.length(); for (int i = 0; i < length; i++) { if (tags.optString(i).equals(tag)) { return true; } } return false; } private void addSystemTag(String tag) { if (TextUtils.isEmpty(tag)) { return; } // Ensure we don't add the same tag again if (!hasSystemTag(tag)) { getSystemTags().put(tag); } } private void removeSystemTag(String tag) { if (!hasSystemTag(tag)) { return; } JSONArray tags = getSystemTags(); JSONArray newTags = new JSONArray(); int length = tags.length(); try { for (int i = 0; i < length; i++) { Object val = tags.get(i); if (!val.equals(tag)) newTags.put(val); } } catch (JSONException e) { // could not update pinned setting } setProperty(SYSTEM_TAGS_PROPERTY, newTags); } /** * Check if the note has any changes * * @param content the new note content * @param tagString space separated tags * @param isPinned note is pinned * @param isMarkdownEnabled note has markdown enabled * @return true if note has changes, false if it is unchanged. */ public boolean hasChanges(String content, String tagString, boolean isPinned, boolean isMarkdownEnabled) { return !content.equals(this.getContent()) || !tagString.equals(this.getTagString().toString()) || this.isPinned() != isPinned || this.isMarkdownEnabled() != isMarkdownEnabled; } public static class Schema extends BucketSchema<Note> { protected static NoteIndexer sNoteIndexer = new NoteIndexer(); protected static NoteFullTextIndexer sFullTextIndexer = new NoteFullTextIndexer(); public Schema() { autoIndex(); addIndex(sNoteIndexer); setupFullTextIndex(sFullTextIndexer, NoteFullTextIndexer.INDEXES); setDefault(CONTENT_PROPERTY, ""); setDefault(SYSTEM_TAGS_PROPERTY, new JSONArray()); setDefault(TAGS_PROPERTY, new JSONArray()); setDefault(DELETED_PROPERTY, false); setDefault(SHARE_URL_PROPERTY, ""); setDefault(PUBLISH_URL_PROPERTY, ""); } public String getRemoteName() { return Note.BUCKET_NAME; } public Note build(String key, JSONObject properties) { return new Note(key, properties); } public void update(Note note, JSONObject properties) { note.setProperties(properties); note.mTitle = null; note.mContentPreview = null; } } }