/*
* Copyright (c) 2013 Andrew Fontaine, James Finlay, Jesse Tucker, Jacob Viau, and
* Evan DeGraff
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package ca.cmput301f13t03.adventure_datetime.model;
import ca.cmput301f13t03.adventure_datetime.model.Interfaces.IWebStorage;
import io.searchbox.Action;
import io.searchbox.client.JestClient;
import io.searchbox.client.JestResult;
import io.searchbox.core.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Class for interacting with the ES service
* The latest error string can be retrieved via getErrorString
*/
public class WebStorage implements IWebStorage {
private static final String MATCH_ALL =
"{\n" +
" \"from\" : %s, \"size\" : %s,\n" +
"%s" + // For an optional SORT. No newline on purpose
" \"query\" : {\n" +
" \"match_all\" : {}\n" +
" }\n" +
"}";
private static final String MATCH =
"{\n" +
" \"from\" : %s, \"size\" : %s,\n" +
"%s" + // For an optional SORT. No newline on purpose
" \"query\" : {\n" +
" \"match\": {\n" +
" \"%s\" : {\n" +
" \"query\" : \"%s\",\n" +
" \"type\" : \"boolean\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
private static final String MULTI_MATCH =
"{\n" +
" \"from\" : %s, \"size\" : %s,\n" +
"%s" + // For an optional SORT. No newline on purpose
" \"query\" : {\n" +
" \"multi_match\" : {\n" +
" \"query\" : \"%s\",\n" +
" \"fields\" : [\"author^3\", \"title^3\", \"tags^2\", \"synopsis\"]\n" +
" }\n" +
" }\n" +
"}";
private static final String MULTI_ID =
"{\n" +
" \"from\" : %s, \"size\" : %s,\n" +
"%s" + // For an optional SORT. No newline on purpose
" \"query\" : {\n" +
" \"ids\" : {\n" +
" \"values\" : [%s]\n" +
" }\n" +
" }\n" +
"}";
private static final String SORT =
" \"sort\": [\n" +
" {\n" +
" \"%s\": {\n" +
" \"order\": \"%s\"\n" +
" }\n" +
" }\n" +
" ],\n";
private static final String defaultIndex = "cmput301f13t03";
private JestClient client;
private String errorMessage;
private String jsonString;
private String _index;
/**
* Construct a basic WebStorage object
*/
public WebStorage() {
client = ES.Client.getClient();
_index = defaultIndex;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#getStory(java.util.UUID)
*/
@Override
public Story getStory(UUID storyId) throws Exception {
Get get = new Get.Builder(_index, storyId.toString()).type("story").build();
JestResult result = execute(get);
Story story = result.getSourceAsObject(Story.class);
Image image = getImage(storyId);
if (image != null)
story.setThumbnail(image.getEncodedBitmap());
return story;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#getStories(int, int)
*/
@Override
public List<Story> getStories(int from, int size) throws Exception {
String sort = String.format(SORT, "timestamp", "desc");
String query = String.format(MATCH_ALL, from, size, sort);
Search search = new Search.Builder(query)
.addIndex(_index)
.addType("story")
.build();
JestResult result = execute(search);
List<Story> stories = result.getSourceAsObjectList(Story.class);
mapImagesToStories(stories);
return stories;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#queryStories(Java.lang.String, int, int)
*/
public List<Story> queryStories(String filter, int from, int size) throws Exception {
String sort = String.format(SORT, "timestamp", "desc");
String query = String.format(MULTI_MATCH, from, size, sort, filter);
Search search = new Search.Builder(query)
.addIndex(_index)
.addType("story")
.build();
JestResult result = execute(search);
List<Story> stories = result.getSourceAsObjectList(Story.class);
mapImagesToStories(stories);
return stories;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#getFragment(java.util.UUID)
*/
@Override
public StoryFragment getFragment(UUID fragmentId) throws Exception {
Get get = new Get.Builder(_index, fragmentId.toString()).type("fragment").build();
JestResult result = execute(get);
StoryFragment fragment = result.getSourceAsObject(StoryFragment.class);
mapImagesToFragment(fragment);
return fragment;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#getFragmentsForStory(java.util.UUID, int, int)
*/
@Override
public List<StoryFragment> getFragmentsForStory(UUID storyId, int from, int size) throws Exception {
Search search = new Search.Builder(
String.format(MATCH, from, size, "", "storyID", storyId))
.addIndex(_index)
.addType("fragment")
.build();
JestResult result = execute(search);
List<StoryFragment> fragments = result.getSourceAsObjectList(StoryFragment.class);
mapImagesToFragments(fragments);
return fragments;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#getComments(java.util.UUID, int from, int size)
*/
@Override
public List<Comment> getComments(UUID targetId, int from, int size) throws Exception {
String sort = String.format(SORT, "timestamp", "desc");
String query = String.format(MATCH, from, size, sort, "targetId", targetId);
Search search = new Search.Builder(query)
.addIndex(_index)
.addType("comment")
.build();
JestResult result = execute(search);
List<Comment> comments = result.getSourceAsObjectList(Comment.class);
mapImagesToComments(comments);
return comments;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#putComment(ca.cmput301f13t03.adventure_datetime.model.Comment)
*/
@Override
public boolean putComment(Comment comment) throws Exception {
Index index = new Index.Builder(comment)
.index(_index)
.type("comment")
.id(comment.getId().toString())
.build();
JestResult result = execute(index);
boolean success = result.isSucceeded();
if (comment.getImage() != null) {
success &= putImage(comment.getImage());
}
return success;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#deleteComment(java.util.UUID)
*/
@Override
public boolean deleteComment(UUID commentId) throws Exception {
JestResult result = execute(new Delete.Builder(commentId.toString())
.index(_index)
.type("comment").build());
// try to delete Image
try {
deleteImage(commentId);
}
catch (Exception e) {
// I don't care if this fails
}
return result.isSucceeded();
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#publishStory(ca.cmput301f13t03.adventure_datetime.model.Story, java.util.List)
*/
@Override
public boolean publishStory(Story story, List<StoryFragment> fragments) throws Exception {
boolean succeeded;
// I am not cleaning up old fragments because I am assuming we do not support
// deleting fragments. If we do support that, then I will have to clean them up.
Index index = new Index.Builder(story)
.index(_index)
.type("story")
.id(story.getId().toString())
.build();
JestResult result = execute(index);
succeeded = result.isSucceeded();
if (story.getThumbnail() != null) {
succeeded &= putImage(story.getThumbnail());
}
Bulk.Builder bulkBuilder = new Bulk.Builder()
.defaultIndex(_index)
.defaultType("fragment");
if (fragments != null) {
for (StoryFragment f : fragments) {
f.updateMediaIds();
bulkBuilder.addAction(new Index.Builder(f).id(f.getFragmentID().toString()).build());
}
result = execute(bulkBuilder.build());
succeeded &= result.isSucceeded();
for (StoryFragment f : fragments) {
succeeded &= putImages(f.getStoryMedia());
}
}
return succeeded;
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#getImage(java.util.UUID)
*/
@Override
public Image getImage(UUID imageId) throws Exception {
Get get = new Get.Builder(_index, imageId.toString()).type("image").build();
JestResult result = execute(get);
return result.getSourceAsObject(Image.class);
}
/**
* Gets images in bulk
* @param imageIds the list of images to retrieve
* @return A list of images
* @throws Exception
*/
public List<Image> getImages(List<UUID> imageIds) throws Exception {
if (imageIds == null || imageIds.size() == 0)
return new ArrayList<Image>();
StringBuilder ids = new StringBuilder();
for (UUID id : imageIds) {
ids.append(String.format("\"%s\", ", id));
}
// Trim the last comma and space
ids.delete(ids.length() - 2, ids.length());
Search search = new Search.Builder(
String.format(MULTI_ID, 0, imageIds.size(), "", ids))
.addIndex(_index)
.addType("image")
.build();
JestResult result = execute(search);
return result.getSourceAsObjectList(Image.class);
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#putImage(ca.cmput301f13t03.adventure_datetime.model.Image)
*/
@Override
public boolean putImage(Image image) throws Exception {
Index index = new Index.Builder(image)
.index(_index)
.type("image")
.id(image.getId().toString())
.build();
JestResult result = execute(index);
return result.isSucceeded();
}
private boolean putImages(List<Image> images) throws Exception {
if (images == null)
return true;
Bulk.Builder bulkBuilder = new Bulk.Builder()
.defaultIndex(_index)
.defaultType("image");
for(Image i : images) {
bulkBuilder.addAction(new Index.Builder(i).id(i.getId().toString()).build());
}
JestResult result = execute(bulkBuilder.build());
return result.isSucceeded();
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#deleteImage(java.util.UUID)
*/
@Override
public boolean deleteImage(UUID imageId) throws Exception {
Delete delete = new Delete.Builder(imageId.toString())
.index(_index)
.type("image")
.build();
JestResult result = execute(delete);
return result.isSucceeded();
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#deleteStory(java.util.UUID)
*/
@Override
public boolean deleteStory(UUID storyId) throws Exception {
JestResult result = execute(new Delete.Builder(storyId.toString())
.index(_index)
.type("story").build());
// try to delete Image
try {
deleteImage(storyId);
}
catch (Exception e) {
// I don't care if this fails
}
return result.isSucceeded();
}
/* (non-Javadoc)
* @see ca.cmput301f13t03.adventure_datetime.model.IWebStorage#deleteFragment(java.util.UUID)
*/
@Override
public boolean deleteFragment(UUID fragId) throws Exception {
JestResult result = execute(new Delete.Builder(fragId.toString())
.index(_index)
.type("fragment").build());
return result.isSucceeded();
}
/**
* The latest ErrorMessage, or null if none exist.
* @return The latest error message, or null if none
*/
public String getErrorMessage() {
return this.errorMessage;
}
/**
* The latest pure JSON result from the ES server
* @return a JSON string returned from the server
*/
public String getJsonString() {
return jsonString;
}
/**
* Gets the current index being used
* @return The index in ES we are using
*/
public String getIndex() {
return this._index;
}
/**
* Sets the index to run against
* For production use "setDefaultIndex()"
* For test, put "test" in here.
*/
public void setIndex(String index) {
this._index = index;
}
/**
* Sets the index to the default production index
*/
public void setDefaultIndex() {
this._index = defaultIndex;
}
/**
* Execute a client action and set this WebRequester's error message
* @param clientRequest the Action to execute
* @return The JestResult
* @throws Exception, connection errors, etc. See JestClient
*/
private JestResult execute(Action clientRequest) throws Exception {
JestResult result = client.execute(clientRequest);
this.errorMessage = result.getErrorMessage();
this.jsonString = result.getJsonString();
return result;
}
private void mapImagesToStories(List<Story> stories) throws Exception {
if (stories == null || stories.size() == 0)
return;
// Get the thumbnails
List<UUID> ids = new ArrayList<UUID>();
for (Story s : stories) {
ids.add(s.getId());
}
List<Image> images = getImages(ids);
for (Image i : images) {
for (Story s : stories) {
if (s.getId().equals(i.getId())) {
s.setThumbnail(i.getEncodedBitmap());
break;
}
}
}
}
private void mapImagesToComments(List<Comment> comments) throws Exception {
if (comments == null || comments.size() == 0)
return;
// Get the thumbnails
List<UUID> ids = new ArrayList<UUID>();
for (Comment c : comments) {
ids.add(c.getId());
}
List<Image> images = getImages(ids);
for (Image i : images) {
for (Comment c : comments) {
if (c.getId().equals(i.getId())) {
c.setImage(i.getEncodedBitmap());
break;
}
}
}
}
private void mapImagesToFragments(List<StoryFragment> fragments) throws Exception {
for (StoryFragment f : fragments) {
mapImagesToFragment(f);
}
}
private void mapImagesToFragment(StoryFragment fragment) throws Exception {
if (fragment.getMediaIds() == null)
return;
List<Image> images = getImages(fragment.getMediaIds());
fragment.setStoryMedia(new ArrayList<Image>(images));
}
}