You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by li...@apache.org on 2010/08/27 14:00:53 UTC

svn commit: r990115 - in /shindig/trunk: content/sampledata/ java/social-api/src/main/java/org/apache/shindig/social/core/config/ java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/ java/social-api/src/main/java/org/apache/shind...

Author: lindner
Date: Fri Aug 27 12:00:52 2010
New Revision: 990115

URL: http://svn.apache.org/viewvc?rev=990115&view=rev
Log:
SHINDIG-1410 | Patch from Eric Woods | Album/MediaItem implementation

Added:
    shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AlbumHandler.java
    shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MediaItemHandler.java
    shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AlbumService.java
    shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MediaItemService.java
Modified:
    shindig/trunk/content/sampledata/canonicaldb.json
    shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/SocialApiGuiceModule.java
    shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/SampleModule.java
    shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialService.java

Modified: shindig/trunk/content/sampledata/canonicaldb.json
URL: http://svn.apache.org/viewvc/shindig/trunk/content/sampledata/canonicaldb.json?rev=990115&r1=990114&r2=990115&view=diff
==============================================================================
--- shindig/trunk/content/sampledata/canonicaldb.json (original)
+++ shindig/trunk/content/sampledata/canonicaldb.json Fri Aug 27 12:00:52 2010
@@ -369,6 +369,25 @@
 		}
 	}]
 },
+"albums" : {
+	"john.doe": [{
+		"id" : "album123",
+		"ownerId" : "john.doe",
+  		"thumbnailUrl" : "http://pages.example.org/albums/4433221-tn.png",
+  		"title" : "Example Album",
+  		"description" : "This is an example album, and this text is an example description",
+  		"location" : { "latitude": 0, "longitude": 0 }
+	}]
+},
+"mediaItems" : {
+	"john.doe": [{
+		"id" : "mediaItem123",
+		"albumId" : "album123",
+		"mimeType" : "image/jpeg",
+		"type" : "image",
+		"url" : "http://animals.nationalgeographic.com/staticfiles/NGS/Shared/StaticFiles/animals/images/primary/black-spider-monkey.jpg"
+	}]
+},
 //
 // ----------------------------- Data ---------------------------------------
 //

Modified: shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/SocialApiGuiceModule.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/SocialApiGuiceModule.java?rev=990115&r1=990114&r2=990115&view=diff
==============================================================================
--- shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/SocialApiGuiceModule.java (original)
+++ shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/SocialApiGuiceModule.java Fri Aug 27 12:00:52 2010
@@ -18,6 +18,9 @@
 
 package org.apache.shindig.social.core.config;
 
+import java.util.List;
+import java.util.Set;
+
 import org.apache.shindig.auth.AnonymousAuthenticationHandler;
 import org.apache.shindig.auth.AuthenticationHandler;
 import org.apache.shindig.common.servlet.ParameterFetcher;
@@ -30,13 +33,12 @@ import org.apache.shindig.social.core.oa
 import org.apache.shindig.social.core.util.BeanXStreamAtomConverter;
 import org.apache.shindig.social.core.util.xstream.XStream081Configuration;
 import org.apache.shindig.social.opensocial.service.ActivityHandler;
+import org.apache.shindig.social.opensocial.service.AlbumHandler;
 import org.apache.shindig.social.opensocial.service.AppDataHandler;
+import org.apache.shindig.social.opensocial.service.MediaItemHandler;
 import org.apache.shindig.social.opensocial.service.MessageHandler;
 import org.apache.shindig.social.opensocial.service.PersonHandler;
 
-import java.util.List;
-import java.util.Set;
-
 import com.google.common.collect.ImmutableSet;
 import com.google.inject.AbstractModule;
 import com.google.inject.TypeLiteral;
@@ -83,6 +85,6 @@ public class SocialApiGuiceModule extend
    */
   protected Set<Class<?>> getHandlers() {
     return ImmutableSet.<Class<?>>of(ActivityHandler.class, AppDataHandler.class,
-        PersonHandler.class, MessageHandler.class);
+        PersonHandler.class, MessageHandler.class, AlbumHandler.class, MediaItemHandler.class);
   }
 }

Added: shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AlbumHandler.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AlbumHandler.java?rev=990115&view=auto
==============================================================================
--- shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AlbumHandler.java (added)
+++ shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AlbumHandler.java Fri Aug 27 12:00:52 2010
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.model.Album;
+import org.apache.shindig.social.opensocial.spi.AlbumService;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+/*
+ * Receives and delegates requests to the OpenSocial Album service.
+ * 
+ * TODO: test cases
+ */
+@Service(name = "albums", path = "/{userId}+/{groupId}/{albumId}+")
+public class AlbumHandler {
+
+	private final AlbumService service;
+	private final ContainerConfig config;
+
+	@Inject
+	public AlbumHandler(AlbumService service, ContainerConfig config) {
+		this.service = service;
+		this.config = config;
+	}
+
+	/*
+	 * Handles create operations.
+	 * 
+	 * Allowed end-points: /albums/{userId}/@self
+	 * 
+	 * Examples: /albums/john.doe/@self
+	 */
+	@Operation(httpMethods = "POST", bodyParam = "album")
+	public Future<?> create(SocialRequestItem request) throws ProtocolException {
+		// Retrieve userIds and albumIds
+		Set<UserId> userIds = request.getUsers();
+		List<String> albumIds = request.getListParameter("albumId");
+
+		// Preconditions - exactly one userId specified, no albumIds specified
+		HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+		HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+		HandlerPreconditions.requireEmpty(albumIds, "Cannot specify albumId in create");
+
+		return service.createAlbum(Iterables.getOnlyElement(userIds),
+				request.getAppId(),
+				request.getTypedParameter("album", Album.class),
+				request.getToken());
+	}
+
+	/*
+	 * Handles retrieve operations.
+	 * 
+	 * Allowed end-points: /albums/{userId}+/{groupId}/{albumId}+
+	 * 
+	 * Examples: /albums/@me/@self /albums/john.doe/@self/1,2
+	 * /albums/john.doe,jane.doe/@friends
+	 */
+	@Operation(httpMethods = "GET")
+	public Future<?> get(SocialRequestItem request) throws ProtocolException {
+		// Get user, group, and album IDs
+		Set<UserId> userIds = request.getUsers();
+		Set<String> optionalAlbumIds = ImmutableSet.copyOf(request
+				.getListParameter("albumId"));
+
+		// At least one userId must be specified
+		HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+
+		// If multiple userIds specified, albumIds must not be specified
+		if (userIds.size() > 1 && !optionalAlbumIds.isEmpty()) {
+			throw new IllegalArgumentException("Cannot fetch same albumIds for multiple userIds");
+		}
+
+		// Retrieve albums by ID
+		if (!optionalAlbumIds.isEmpty()) {
+			if (optionalAlbumIds.size() == 1) {
+				return service.getAlbum(Iterables.getOnlyElement(userIds),
+						request.getAppId(), request.getFields(),
+						optionalAlbumIds.iterator().next(), request.getToken());
+			} else {
+				return service.getAlbums(Iterables.getOnlyElement(userIds),
+						request.getAppId(), request.getFields(),
+						new CollectionOptions(request), optionalAlbumIds,
+						request.getToken());
+			}
+		}
+
+		// Retrieve albums by group
+		return service.getAlbums(userIds, request.getGroup(), request
+				.getAppId(), request.getFields(),
+				new CollectionOptions(request), request.getToken());
+	}
+
+	/*
+	 * Handles update operations.
+	 * 
+	 * Allowed end-points: /albums/{userId}/@self/{albumId}
+	 * 
+	 * Examples: /albums/john.doe/@self/1
+	 */
+	@Operation(httpMethods = "PUT", bodyParam = "album")
+	public Future<?> update(SocialRequestItem request) throws ProtocolException {
+		// Retrieve userIds and albumIds
+		Set<UserId> userIds = request.getUsers();
+		List<String> albumIds = request.getListParameter("albumId");
+
+		// Enforce preconditions - exactly one user and one album specified
+		HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+		HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+		HandlerPreconditions.requireNotEmpty(albumIds, "No albumId specified");
+		HandlerPreconditions.requireSingular(albumIds, "Multiple albumIds not supported");
+
+		return service.updateAlbum(Iterables.getOnlyElement(userIds),
+				request.getAppId(),
+				request.getTypedParameter("album", Album.class),
+				Iterables.getOnlyElement(albumIds), request.getToken());
+	}
+
+	/*
+	 * Handles delete operations.
+	 * 
+	 * Allowed end-points: /albums/{userId}/@self/{albumId}
+	 * 
+	 * Examples: /albums/john.doe/@self/1
+	 */
+	@Operation(httpMethods = "DELETE")
+	public Future<?> delete(SocialRequestItem request) throws ProtocolException {
+		// Get user and album ID
+		Set<UserId> userIds = request.getUsers();
+		String albumId = request.getParameter("albumId");
+
+		// Enforce preconditions - userIds must contain exactly one element
+		HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+		HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+		// Service request
+		return service.deleteAlbum(Iterables.getOnlyElement(userIds),
+				request.getAppId(), albumId, request.getToken());
+	}
+	
+	/*
+	 * Retrieves supported fields for the albums service.
+	 */
+	@Operation(httpMethods = "GET", path = "/@supportedFields")
+	public List<Object> supportedFields(RequestItem request) {
+		String container = firstNonNull(request.getToken().getContainer(),
+				ContainerConfig.DEFAULT_CONTAINER);
+		return config.getList(container,
+				"${Cur['gadgets.features'].opensocial.supportedFields.album}");
+	}
+
+	private static <T> T firstNonNull(T first, T second) {
+		return first != null ? first : Preconditions.checkNotNull(second);
+	}
+}
\ No newline at end of file

Added: shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MediaItemHandler.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MediaItemHandler.java?rev=990115&view=auto
==============================================================================
--- shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MediaItemHandler.java (added)
+++ shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MediaItemHandler.java Fri Aug 27 12:00:52 2010
@@ -0,0 +1,202 @@
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.MediaItemService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+/*
+ * Receives and delegates requests to the OpenSocial MediaItems service.
+ * 
+ * TODO: test cases
+ */
+@Service(name = "mediaItems", path = "/{userId}+/{groupId}/{albumId}/{mediaItemId}+")
+public class MediaItemHandler {
+
+	
+	private final MediaItemService service;
+	private final ContainerConfig config;
+	
+	@Inject
+	public MediaItemHandler(MediaItemService service, ContainerConfig config) {
+		this.service = service;
+		this.config = config;
+	}
+	
+	/*
+	 * Handles GET operations.
+	 * 
+	 * Allowed end-points: /mediaItems/{userId}+/{groupId}/{albumId}/{mediaItemId}+
+	 * 
+	 * Examples:	/mediaItems/john.doe/@self
+	 * 				/mediaItems/john.doe,jane.doe/@self
+	 * 				/mediaItems/john.doe/@self/album123
+	 * 				/mediaItems/john.doe/@self/album123/1,2,3	
+	 */
+	@Operation(httpMethods = "GET")
+	public Future<?> get(SocialRequestItem request) throws ProtocolException {
+		// Get user, group, album IDs, and MediaItem IDs
+		Set<UserId> userIds = request.getUsers();
+		Set<String> optionalAlbumIds = ImmutableSet.copyOf(request.getListParameter("albumId"));
+		Set<String> optionalMediaItemIds = ImmutableSet.copyOf(request.getListParameter("mediaItemId"));
+		
+		// At least one userId must be specified
+		HandlerPreconditions.requireNotEmpty(userIds, "No user ID specified");
+		
+		// Get Album ID; null if not provided
+		String albumId = null;
+		if (optionalAlbumIds.size() == 1) {
+			albumId = Iterables.getOnlyElement(optionalAlbumIds);
+		} else if (optionalAlbumIds.size() > 1) {
+			throw new IllegalArgumentException("Multiple Album IDs not supported");
+		}
+		
+		// Cannot retrieve by ID if album ID not provided
+		if (albumId == null && !optionalMediaItemIds.isEmpty()) {
+			throw new IllegalArgumentException("Cannot fetch by MediaItem ID without Album ID");
+		}
+		
+		// Cannot retrieve by ID or album if multiple user's given
+		if (userIds.size() > 1) {
+			if (!optionalMediaItemIds.isEmpty()) {
+				throw new IllegalArgumentException("Cannot fetch MediaItem by ID for multiple users");
+			} else if (albumId != null) {
+				throw new IllegalArgumentException("Cannot fetch MediaItem by Album for multiple users");
+			} 
+		}
+		
+		// Retrieve by ID(s)
+		if (!optionalMediaItemIds.isEmpty()) {
+			if (optionalMediaItemIds.size() == 1) {
+				return service.getMediaItem(Iterables.getOnlyElement(userIds),
+						request.getAppId(), albumId,
+						Iterables.getOnlyElement(optionalMediaItemIds),
+						request.getFields(), request.getToken());
+			} else {
+				return service.getMediaItems(Iterables.getOnlyElement(userIds),
+						request.getAppId(), albumId, optionalMediaItemIds,
+						request.getFields(), new CollectionOptions(request),
+						request.getToken());
+			}
+		}
+		
+		// Retrieve by Album
+		if (albumId != null) {
+			return service.getMediaItems(Iterables.getOnlyElement(userIds),
+					request.getAppId(), albumId, request.getFields(),
+					new CollectionOptions(request), request.getToken());
+		}
+		
+		// Retrieve by users and groups
+		return service.getMediaItems(userIds, request.getGroup(), request
+				.getAppId(), request.getFields(),
+				new CollectionOptions(request), request.getToken());
+	}
+	
+	/*
+	 * Handles DELETE operations.
+	 * 
+	 * Allowed end-points: /mediaItem/{userId}/@self/{albumId}/{mediaItemId}
+	 * 
+	 * Examples: /mediaItems/john.doe/@self/1/2
+	 */
+	@Operation(httpMethods = "DELETE")
+	public Future<?> delete(SocialRequestItem request) throws ProtocolException {
+		// Get users, Album ID, and MediaItem ID
+		Set<UserId> userIds = request.getUsers();
+		Set<String> albumIds = ImmutableSet.copyOf(request.getListParameter("albumId"));
+		Set<String> mediaItemIds = ImmutableSet.copyOf(request.getListParameter("mediaItemId"));
+		
+		// Exactly one user, Album, and MediaItem must be specified
+		HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+		HandlerPreconditions.requireSingular(userIds, "Exactly one user ID must be specified");
+		HandlerPreconditions.requireSingular(albumIds, "Exactly one Album ID must be specified");
+		HandlerPreconditions.requireSingular(mediaItemIds, "Exactly one MediaItem ID must be specified");
+		
+		// Service request
+		return service.deleteMediaItem(Iterables.getOnlyElement(userIds),
+				request.getAppId(), Iterables.getOnlyElement(albumIds),
+				Iterables.getOnlyElement(mediaItemIds), request.getToken());
+	}
+	
+	/*
+	 * Handles POST operations.
+	 * 
+	 * Allowed end-points: /mediaItems/{userId}/@self/{albumId}
+	 * 
+	 * Examples: /mediaItems/john.doe/@self/1
+	 */
+	@Operation(httpMethods = "POST", bodyParam = "mediaItem")
+	public Future<?> create(SocialRequestItem request) throws ProtocolException {
+		// Retrieve userIds and albumIds
+		Set<UserId> userIds = request.getUsers();
+		Set<String> albumIds = ImmutableSet.copyOf(request.getListParameter("albumId"));
+		
+		// Exactly one user and Album must be specified
+		HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+		HandlerPreconditions.requireSingular(userIds, "Exactly one user ID must be specified");
+		HandlerPreconditions.requireSingular(albumIds, "Exactly one Album ID must be specified");
+		
+		// Service request
+		return service.createMediaItem(Iterables.getOnlyElement(userIds),
+				request.getAppId(), Iterables.getOnlyElement(albumIds),
+				request.getTypedParameter("mediaItem", MediaItem.class),
+				request.getToken());
+	}
+	
+	/*
+	 * Handles PUT operations.
+	 * 
+	 * Allowed end-points: /mediaItems/{userId}/@self/{albumId}/{mediaItemId}
+	 * 
+	 * Examples: /mediaItems/john.doe/@self/1/2
+	 */
+	@Operation(httpMethods = "PUT", bodyParam = "mediaItem")
+	public Future<?> update(SocialRequestItem request) throws ProtocolException {
+		// Retrieve userIds, albumIds, and mediaItemIds
+		Set<UserId> userIds = request.getUsers();
+		Set<String> albumIds = ImmutableSet.copyOf(request.getListParameter("albumId"));
+		Set<String> mediaItemIds = ImmutableSet.copyOf(request.getListParameter("mediaItemIds"));
+		
+		// Exactly one user, Album, and MediaItem must be specified
+		HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+		HandlerPreconditions.requireSingular(userIds, "Exactly one user ID must be specified");
+		HandlerPreconditions.requireSingular(albumIds, "Exactly one Album ID must be specified");
+		HandlerPreconditions.requireSingular(mediaItemIds, "Exactly one MediaItem ID must be specified");
+		
+		// Service request
+		return service.updateMediaItem(Iterables.getOnlyElement(userIds),
+				request.getAppId(), Iterables.getOnlyElement(albumIds),
+				Iterables.getOnlyElement(mediaItemIds),
+				request.getTypedParameter("mediaItem", MediaItem.class),
+				request.getToken());
+	}
+
+	@Operation(httpMethods = "GET", path = "/@supportedFields")
+	public List<Object> supportedFields(RequestItem request) {
+		// TODO: Would be nice if name in config matched name of service.
+		String container = firstNonNull(request.getToken().getContainer(),
+				ContainerConfig.DEFAULT_CONTAINER);
+		return config.getList(container,
+			"${Cur['gadgets.features'].opensocial.supportedFields.mediaItem}");
+	}
+
+	private static <T> T firstNonNull(T first, T second) {
+		return first != null ? first : Preconditions.checkNotNull(second);
+	}
+}

Added: shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AlbumService.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AlbumService.java?rev=990115&view=auto
==============================================================================
--- shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AlbumService.java (added)
+++ shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AlbumService.java Fri Aug 27 12:00:52 2010
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.social.opensocial.model.Album;
+
+/*
+ * The AlbumService interface defines the service provider interface for
+ * creating, retrieving, updating, and deleting OpenSocial albums.
+ */
+public interface AlbumService {
+
+	/*
+	 * Retrieves a single album for the given user with the given album ID.
+	 * 
+	 * @param userId	Identifies the person to retrieve the album from
+	 * @param appId		Identifies the application to retrieve the album from
+	 * @param fields	Indicates the fields to return.  Empty set implies all
+	 * @param albumId	Identifies the album to retrieve
+	 * @param token		A valid SecurityToken
+	 * 
+	 * @return a response item with the requested album
+	 */
+	Future<Album> getAlbum(UserId userId, String appId, Set<String> fields,
+			String albumId, SecurityToken token) throws ProtocolException;
+
+	/*
+	 * Retrieves albums for the given user with the given album IDs.
+	 * 
+	 * @param userId	Identifies the person to retrieve albums for
+	 * @param appId		Identifies the application to retrieve albums from
+	 * @param fields	The fields to return; empty set implies all
+	 * @param options	The sorting/filtering/pagination options
+	 * @param albumIds	The set of album ids to fetch
+	 * @param token		A valid SecurityToken
+	 * 
+	 * @return a response item with requested albums
+	 */
+	Future<RestfulCollection<Album>> getAlbums(UserId userId, String appId,
+			Set<String> fields, CollectionOptions options,
+			Set<String> albumIds, SecurityToken token) throws ProtocolException;
+
+	/*
+	 * Retrieves albums for the given user and group.
+	 * 
+	 * @param userIds	Identifies the users to retrieve albums from
+	 * @param groupId	Identifies the group to retrieve albums from
+	 * @param appId		Identifies the application to retrieve albums from
+	 * @param fields	The fields to return.  Empty set implies all
+	 * @param options	The sorting/filtering/pagination options
+	 * @param token		A valid SecurityToken
+	 * 
+	 * @return a response item with the requested albums
+	 */
+	Future<RestfulCollection<Album>> getAlbums(Set<UserId> userIds,
+			GroupId groupId, String appId, Set<String> fields,
+			CollectionOptions options, SecurityToken token)
+			throws ProtocolException;
+	
+	/*
+	 * Deletes a single album for the given user with the given album ID.
+	 * 
+	 * @param userId	Identifies the user to delete the album from
+	 * @param appId		Identifies the application to delete the album from
+	 * @param albumId	Identifies the album to delete
+	 * @param token		A valid SecurityToken
+	 * 
+	 * @return a response item containing any errors
+	 */
+	Future<Void> deleteAlbum(UserId userId, String appId, String albumId,
+			SecurityToken token) throws ProtocolException;
+	
+	/*
+	 * Creates an album for the given user.
+	 * 
+	 * @param userId	Identifies the user to create the album for
+	 * @param appId		Identifies the application to create the album in
+	 * @param album		The album to create
+	 * @param token		A valid SecurityToken
+	 * 
+	 * @return a response containing any errors
+	 */
+	Future<Void> createAlbum(UserId userId, String appId, Album album,
+			SecurityToken token) throws ProtocolException;
+	
+	/*
+	 * Updates an album for the given user.  The album ID specified in the REST
+	 * end-point is used, even if the album also defines an ID.
+	 * 
+	 * @param userId	Identifies the user to update the album for
+	 * @param appId		Identifies the application to update the album in
+	 * @param album		Defines the updated album
+	 * @param albumId	Identifies the ID of the album to update
+	 * @param token		A valid SecurityToken
+	 * 
+	 * @return a response containing any errors
+	 */
+	Future<Void> updateAlbum(UserId userId, String appId, Album album,
+			String albumId, SecurityToken token) throws ProtocolException;
+}
\ No newline at end of file

Added: shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MediaItemService.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MediaItemService.java?rev=990115&view=auto
==============================================================================
--- shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MediaItemService.java (added)
+++ shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MediaItemService.java Fri Aug 27 12:00:52 2010
@@ -0,0 +1,129 @@
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+
+/*
+ * The MediaItemService interface defines the service provider interface for
+ * creating, retrieving, updating, and deleting OpenSocial MediaItems.
+ */
+public interface MediaItemService {
+	
+	/*
+	 * Retrieves a MediaItem by ID.
+	 * 
+	 * @param userId		Identifies the owner of the MediaItem to retrieve
+	 * @param appId			Identifies the application of the MeiaItem to retrieve
+	 * @param albumId		Identifies the album containing the MediaItem
+	 * @param mediaItemId	Identifies the MediaItem to retrieve
+	 * @param fields		Indicates fields to be returned; empty set implies all
+	 * @param token			A valid SecurityToken
+	 * 
+	 * @return a response item with the requested MediaItem
+	 */
+	Future<MediaItem> getMediaItem(UserId userId, String appId, String albumId,
+			String mediaItemId, Set<String> fields, SecurityToken token)
+			throws ProtocolException;
+	
+	/*
+	 * Retrieves MediaItems by IDs.
+	 * 
+	 * @param userId		Identifies the owner of the MediaItems
+	 * @param appId			Identifies the application of the MediaItems
+	 * @param albumId		Identifies the album containing the MediaItems
+	 * @param mediaItemIds	Identifies the MediaItems to retrieve
+	 * @param fields		Specifies the fields to return; empty set implies all
+	 * @param options		Sorting/filtering/pagination options
+	 * @param token			A valid SecurityToken
+	 * 
+	 * @return a response item with the requested MediaItems
+	 */
+	Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId,
+			String appId, String albumId, Set<String> mediaItemIds,
+			Set<String> fields, CollectionOptions options, SecurityToken token)
+			throws ProtocolException;
+	
+	/*
+	 * Retrieves MediaItems by Album.
+	 * 
+	 * @param userId	Identifies the owner of the MediaItems
+	 * @param appId		Identifies the application of the MediaItems
+	 * @param albumId	Identifies the Album containing the MediaItems
+	 * @param fields	Specifies the fields to return; empty set implies all
+	 * @param options	Sorting/filtering/pagination options
+	 * @param token		A valid SecurityToken
+	 * 
+	 * @return a response item with the requested MediaItems
+	 */
+	Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId,
+			String appId, String albumId, Set<String> fields,
+			CollectionOptions options, SecurityToken token)
+			throws ProtocolException;
+	
+	/*
+	 * Retrieves MediaItems by users and groups.
+	 * 
+	 * @param userIds	Identifies the users that this request is relative to
+	 * @param groupId	Identifies the users' groups to retrieve MediaItems from
+	 * @param appId		Identifies the application to retrieve MediaItems from
+	 * @param fields	The fields to return; empty set implies all
+	 * @param options	Sorting/filtering/pagination options
+	 * @param token		A valid SecurityToken
+	 * 
+	 * @return a response item with the requested MediaItems
+	 */
+	Future<RestfulCollection<MediaItem>> getMediaItems(Set<UserId> userIds,
+			GroupId groupId, String appId, Set<String> fields,
+			CollectionOptions options, SecurityToken token)
+			throws ProtocolException;
+	
+	/*
+	 * Deletes a MediaItem by ID.
+	 * 
+	 * @param userId		Identifies the owner of the MediaItem to delete
+	 * @param appId			Identifies the application hosting the MediaItem
+	 * @param albumId		Identifies the parent album of the MediaItem
+	 * @param mediaItemId	Identifies the MediaItem to delete
+	 * @param token			A valid SecurityToken
+	 * 
+	 * @return a response item containing any errors
+	 */
+	Future<Void> deleteMediaItem(UserId userId, String appId, String albumId,
+			String mediaItemId, SecurityToken token) throws ProtocolException;
+	
+	/*
+	 * Create a MediaItem in the given album for the given user.
+	 * 
+	 * @param userId		Identifies the owner of the MediaItem to create
+	 * @param appId			Identifies the application hosting the MediaItem
+	 * @param albumId		Identifies the album to contain the MediaItem
+	 * @param mediaItem		The MediaItem to create
+	 * @param token			A valid SecurityToken
+	 * 
+	 * @return a response containing any errors
+	 */
+	Future<Void> createMediaItem(UserId userId, String appId, String albumId,
+			MediaItem mediaItem, SecurityToken token) throws ProtocolException;
+	
+	/*
+	 * Updates a MediaItem for the given user.  The MediaItem ID specified in
+	 * the REST end-point is used, even if the MediaItem also defines an ID.
+	 * 
+	 * @param userId		Identifies the owner of the MediaItem to update
+	 * @param appId			Identifies the application hosting the MediaItem
+	 * @param albumId		Identifies the album containing the MediaItem
+	 * @param mediaItemId	Identifies the MediaItem to update
+	 * @param mediaItem		The updated MediaItem to persist
+	 * @param token			A valid SecurityToken
+	 * 
+	 * @return a response containing any errors
+	 */
+	Future<Void> updateMediaItem(UserId userId, String appId, String albumId,
+			String mediaItemId, MediaItem mediaItem, SecurityToken token)
+			throws ProtocolException;
+}

Modified: shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/SampleModule.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/SampleModule.java?rev=990115&r1=990114&r2=990115&view=diff
==============================================================================
--- shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/SampleModule.java (original)
+++ shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/SampleModule.java Fri Aug 27 12:00:52 2010
@@ -19,7 +19,9 @@ package org.apache.shindig.social.sample
 
 import org.apache.shindig.social.opensocial.oauth.OAuthDataStore;
 import org.apache.shindig.social.opensocial.spi.ActivityService;
+import org.apache.shindig.social.opensocial.spi.AlbumService;
 import org.apache.shindig.social.opensocial.spi.AppDataService;
+import org.apache.shindig.social.opensocial.spi.MediaItemService;
 import org.apache.shindig.social.opensocial.spi.MessageService;
 import org.apache.shindig.social.opensocial.spi.PersonService;
 import org.apache.shindig.social.sample.oauth.SampleOAuthDataStore;
@@ -41,10 +43,11 @@ public class SampleModule extends Abstra
     bind(String.class).annotatedWith(Names.named("shindig.canonical.json.db"))
         .toInstance("sampledata/canonicaldb.json");
     bind(ActivityService.class).to(JsonDbOpensocialService.class);
+    bind(AlbumService.class).to(JsonDbOpensocialService.class);
+    bind(MediaItemService.class).to(JsonDbOpensocialService.class);
     bind(AppDataService.class).to(JsonDbOpensocialService.class);
     bind(PersonService.class).to(JsonDbOpensocialService.class);
     bind(MessageService.class).to(JsonDbOpensocialService.class);
-    
     bind(OAuthDataStore.class).to(SampleOAuthDataStore.class);
   }
 }

Modified: shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialService.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialService.java?rev=990115&r1=990114&r2=990115&view=diff
==============================================================================
--- shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialService.java (original)
+++ shindig/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialService.java Fri Aug 27 12:00:52 2010
@@ -18,6 +18,16 @@
 
 package org.apache.shindig.social.sample.spi;
 
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import javax.servlet.http.HttpServletResponse;
+
 import org.apache.commons.io.IOUtils;
 import org.apache.shindig.auth.SecurityToken;
 import org.apache.shindig.common.util.ImmediateFuture;
@@ -28,13 +38,17 @@ import org.apache.shindig.protocol.Restf
 import org.apache.shindig.protocol.conversion.BeanConverter;
 import org.apache.shindig.protocol.model.SortOrder;
 import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.Album;
+import org.apache.shindig.social.opensocial.model.MediaItem;
 import org.apache.shindig.social.opensocial.model.Message;
 import org.apache.shindig.social.opensocial.model.MessageCollection;
 import org.apache.shindig.social.opensocial.model.Person;
 import org.apache.shindig.social.opensocial.spi.ActivityService;
+import org.apache.shindig.social.opensocial.spi.AlbumService;
 import org.apache.shindig.social.opensocial.spi.AppDataService;
 import org.apache.shindig.social.opensocial.spi.CollectionOptions;
 import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.MediaItemService;
 import org.apache.shindig.social.opensocial.spi.MessageService;
 import org.apache.shindig.social.opensocial.spi.PersonService;
 import org.apache.shindig.social.opensocial.spi.UserId;
@@ -42,16 +56,6 @@ import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.Future;
-
-import javax.servlet.http.HttpServletResponse;
-
 import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -65,7 +69,7 @@ import com.google.inject.name.Named;
  */
 @Singleton
 public class JsonDbOpensocialService implements ActivityService, PersonService, AppDataService,
-    MessageService {
+    MessageService, AlbumService, MediaItemService {
 
   private static final Comparator<Person> NAME_COMPARATOR = new Comparator<Person>() {
     public int compare(Person person, Person person1) {
@@ -94,6 +98,16 @@ public class JsonDbOpensocialService imp
    * db["people"] -> Map<Person.Id, Array<Activity>>
    */
   private static final String ACTIVITIES_TABLE = "activities";
+  
+  /**
+   * db["people"] -> Map<Person.Id, Array<Album>>
+   */
+  private static final String ALBUMS_TABLE = "albums";
+  
+  /**
+   * db["people"] -> Map<Person.Id, Array<MediaItem>>
+   */
+  private static final String MEDIAITEMS_TABLE = "mediaItems";
 
   /**
    * db["data"] -> Map<Person.Id, Map<String, String>>
@@ -247,6 +261,7 @@ public class JsonDbOpensocialService imp
         jsonArray = new JSONArray();
         db.getJSONObject(ACTIVITIES_TABLE).put(userId.getUserId(token), jsonArray);
       }
+      // TODO (woodser): if used with PUT, duplicate activity would be created?
       jsonArray.put(jsonObject);
       return ImmediateFuture.newInstance(null);
     } catch (JSONException je) {
@@ -623,19 +638,490 @@ public class JsonDbOpensocialService imp
     }
     return ids;
   }
-
-  private JSONObject convertFromActivity(Activity activity, Set<String> fields)
-      throws JSONException {
-    // TODO Not using fields yet
-    return new JSONObject(converter.convertToString(activity));
-  }
-
-  public <T> T filterFields(JSONObject object, Set<String> fields, Class<T> clz)
-      throws JSONException {
-    if (!fields.isEmpty()) {
-      // Create a copy with just the specified fields
-      object = new JSONObject(object, fields.toArray(new String[fields.size()]));
-    }
-    return converter.convertToObject(object.toString(), clz);
-  }
+  
+  	// TODO: not using appId
+	public Future<Album> getAlbum(UserId userId, String appId, Set<String> fields,
+			String albumId, SecurityToken token) throws ProtocolException {
+		try {
+			// First ensure user has a table
+			String user = userId.getUserId((token));
+			if (db.getJSONObject(ALBUMS_TABLE).has(user)) {
+				// Retrieve user's albums
+				JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+				
+				// Search albums for given ID and owner
+				JSONObject album;
+				for (int i = 0; i < userAlbums.length(); i++) {
+					album = userAlbums.getJSONObject(i);
+					if (album.getString(Album.Field.ID.toString()).equals(albumId) &&
+						album.getString(Album.Field.OWNER_ID.toString()).equals(user)) {
+						return ImmediateFuture.newInstance(filterFields(album, fields, Album.class));
+					}
+				}
+			}
+
+			// Album wasn't found
+			throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO: not using appId
+	public Future<RestfulCollection<Album>> getAlbums(UserId userId, String appId,
+			Set<String> fields, CollectionOptions options, Set<String> albumIds,
+			SecurityToken token) throws ProtocolException {
+		try {
+			// Ensure user has a table
+			String user = userId.getUserId(token);
+			if (db.getJSONObject(ALBUMS_TABLE).has(user)) {
+				// Get user's albums
+				JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+				
+				// Stores target albums
+				List<Album> result = Lists.newArrayList();
+				
+				// Search for every albumId
+				boolean found;
+				JSONObject curAlbum;
+				for (String albumId : albumIds) {
+					// Search albums for this albumId
+					found = false;
+					for (int i = 0; i < userAlbums.length(); i++) {
+						curAlbum = userAlbums.getJSONObject(i);
+						if (curAlbum.getString(Album.Field.ID.toString()).equals(albumId) &&
+							curAlbum.getString(Album.Field.OWNER_ID.toString()).equals(user)) {
+							result.add(filterFields(curAlbum, fields, Album.class));
+							found = true;
+							break;
+						}
+					}
+					
+					// Error - albumId not found
+					if (!found) {
+						throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+					}
+				}
+				
+				// Return found albums
+				return ImmediateFuture.newInstance(new RestfulCollection<Album>(result));
+			}
+			
+			// Album table doesn't exist for user
+			throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "User '" + user + "' has no albums");
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+	
+	// TODO: not using appId
+	public Future<RestfulCollection<Album>> getAlbums(Set<UserId> userIds,
+			GroupId groupId, String appId, Set<String> fields,
+			CollectionOptions options, SecurityToken token)
+			throws ProtocolException {
+		try {
+			List<Album> result = Lists.newArrayList();
+			Set<String> idSet = getIdSet(userIds, groupId, token);
+			
+			// Gather albums for all user IDs
+			for (String id : idSet) {
+				if (db.getJSONObject(ALBUMS_TABLE).has(id)) {
+					JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(id);
+					for (int i = 0; i < userAlbums.length(); i++) {
+						JSONObject album = userAlbums.getJSONObject(i);
+						if (album.getString(Album.Field.OWNER_ID.toString()).equals(id)) {
+							result.add(filterFields(album, fields, Album.class));
+						}
+					}
+				}
+			}
+			return ImmediateFuture.newInstance(new RestfulCollection<Album>(result));
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+	
+	// TODO: not using appId
+	public Future<Void> deleteAlbum(UserId userId, String appId, String albumId,
+			SecurityToken token) throws ProtocolException {
+		try {
+			boolean targetFound = false;			// indicates if target album is found
+			JSONArray newAlbums = new JSONArray();	// list of albums minus target
+			String user = userId.getUserId(token);	// retrieve user id
+			
+			// First ensure user has a table
+			if (db.getJSONObject(ALBUMS_TABLE).has(user)) {
+				// Get user's albums
+				JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+				
+				// Compose new list of albums excluding album to be deleted
+				JSONObject curAlbum;
+				for (int i = 0; i < userAlbums.length(); i++) {
+					curAlbum = userAlbums.getJSONObject(i);
+					if (curAlbum.getString(Album.Field.ID.toString()).equals(albumId)) {
+						targetFound = true;
+					} else {
+						newAlbums.put(curAlbum);
+					}
+				}
+			}
+			
+			// Overwrite user's albums with updated list if album found
+			if (targetFound) {
+				db.getJSONObject(ALBUMS_TABLE).put(user, newAlbums);
+				return ImmediateFuture.newInstance(null);
+			} else {
+				throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+			}
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+	
+	// TODO: userId and album's ownerId don't have to match - potential problem
+	// TODO: not using appId
+	public Future<Void> createAlbum(UserId userId, String appId, Album album,
+			SecurityToken token) throws ProtocolException {
+		try {			
+			// Get table of user's albums
+			String user = userId.getUserId(token);
+			JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+			if (userAlbums == null) {
+				userAlbums = new JSONArray();
+				db.getJSONObject(ALBUMS_TABLE).put(user, userAlbums);
+			}
+			
+			// Convert album to JSON and set ID & owner
+			JSONObject jsonAlbum = convertToJson(album);
+			if (!jsonAlbum.has(Album.Field.ID.toString())) {
+				jsonAlbum.put(Album.Field.ID.toString(), System.currentTimeMillis());
+			}
+			if (!jsonAlbum.has(Album.Field.OWNER_ID.toString())) {
+				jsonAlbum.put(Album.Field.OWNER_ID.toString(), user);
+			}
+			
+			// Insert new album into table
+			userAlbums.put(jsonAlbum);
+			return ImmediateFuture.newInstance(null);
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO: not using appId
+	public Future<Void> updateAlbum(UserId userId, String appId, Album album,
+			String albumId, SecurityToken token) throws ProtocolException {
+		try {
+			// First ensure user has a table
+			String user = userId.getUserId(token);
+			if (db.getJSONObject(ALBUMS_TABLE).has(user)) {
+				// Retrieve user's albums
+				JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+				
+				// Convert album to JSON and set ID
+				JSONObject jsonAlbum = convertToJson(album);
+				jsonAlbum.put(Album.Field.ID.toString(), albumId);
+				
+				// Iterate through albums to identify album to update
+				JSONObject curAlbum = null;
+				for (int i = 0; i < userAlbums.length(); i++) {
+					curAlbum = userAlbums.getJSONObject(i);
+					if (curAlbum.getString(Album.Field.ID.toString()).equals(albumId)) {
+						userAlbums.put(i, jsonAlbum);
+						return ImmediateFuture.newInstance(null);
+					}
+				}
+			}
+
+			// Error - no album found to update with given ID
+			throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO: not using appId
+	public Future<MediaItem> getMediaItem(UserId userId, String appId,
+			String albumId, String mediaItemId, Set<String> fields,
+			SecurityToken token) throws ProtocolException {
+		try {
+			// First ensure user has a table
+			String user = userId.getUserId((token));
+			if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+				// Retrieve user's MediaItems
+				JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+				
+				// Search user's MediaItems for given ID and album
+				JSONObject mediaItem;
+				for (int i = 0; i < userMediaItems.length(); i++) {
+					mediaItem = userMediaItems.getJSONObject(i);
+					if (mediaItem.getString(MediaItem.Field.ID.toString()).equals(mediaItemId) &&
+						mediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+						return ImmediateFuture.newInstance(filterFields(mediaItem, fields, MediaItem.class));
+					}
+				}
+			}
+
+			// MediaItem wasn't found
+			throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem ID '" + mediaItemId + "' does not exist within Album '" + albumId + "'");
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO: not using appId
+	public Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId,
+			String appId, String albumId, Set<String> mediaItemIds,
+			Set<String> fields, CollectionOptions options, SecurityToken token)
+			throws ProtocolException {
+		try {
+			// Ensure user has a table
+			String user = userId.getUserId(token);
+			if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+				// Get user's MediaItems
+				JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+				
+				// Stores found MediaItems
+				List<MediaItem> result = Lists.newArrayList();
+				
+				// Search for every MediaItem ID target
+				boolean found;
+				JSONObject curMediaItem;
+				for (String mediaItemId : mediaItemIds) {
+					// Search existing MediaItems for this MediaItem ID
+					found = false;
+					for (int i = 0; i < userMediaItems.length(); i++) {
+						curMediaItem = userMediaItems.getJSONObject(i);
+						if (curMediaItem.getString(MediaItem.Field.ID.toString()).equals(albumId) &&
+							curMediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+							result.add(filterFields(curMediaItem, fields, MediaItem.class));
+							found = true;
+							break;
+						}
+					}
+					
+					// Error - MediaItem ID not found
+					if (!found) {
+						throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem ID " + mediaItemId + " does not exist within Album " + albumId);
+					}
+				}
+			
+				// Return found MediaItems
+				return ImmediateFuture.newInstance(new RestfulCollection<MediaItem>(result));
+			}
+			
+			// Table doesn't exist for user
+			throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem table not found for user " + user);
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+	
+	// TODO: not using appId
+	public Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId,
+			String appId, String albumId, Set<String> fields,
+			CollectionOptions options, SecurityToken token)
+			throws ProtocolException {
+		try {
+			// First ensure user has a table
+			String user = userId.getUserId((token));
+			if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+				// Retrieve user's MediaItems
+				JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+				
+				// Stores target MediaItems
+				List<MediaItem> result = Lists.newArrayList();
+				
+				// Search user's MediaItems for given album
+				JSONObject curMediaItem;
+				for (int i = 0; i < userMediaItems.length(); i++) {
+					curMediaItem = userMediaItems.getJSONObject(i);
+					if (curMediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+						result.add(filterFields(curMediaItem, fields, MediaItem.class));
+					}
+				}
+				
+				// Return found MediaItems
+				return ImmediateFuture.newInstance(new RestfulCollection<MediaItem>(result));
+			}
+
+			// Album wasn't found
+			throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO: not using appId
+	public Future<RestfulCollection<MediaItem>> getMediaItems(
+			Set<UserId> userIds, GroupId groupId, String appId,
+			Set<String> fields, CollectionOptions options, SecurityToken token)
+			throws ProtocolException {
+		try {
+			List<MediaItem> result = Lists.newArrayList();
+			Set<String> idSet = getIdSet(userIds, groupId, token);
+			
+			// Gather MediaItems for all user IDs
+			for (String id : idSet) {
+				if (db.getJSONObject(MEDIAITEMS_TABLE).has(id)) {
+					JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(id);
+					for (int i = 0; i < userMediaItems.length(); i++) {
+						result.add(filterFields(userMediaItems.getJSONObject(i), fields, MediaItem.class));
+					}
+				}
+			}
+			return ImmediateFuture.newInstance(new RestfulCollection<MediaItem>(result));
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO: not using appId
+	public Future<Void> deleteMediaItem(UserId userId, String appId,
+			String albumId, String mediaItemId, SecurityToken token)
+			throws ProtocolException {
+		try {
+			boolean targetFound = false;				// indicates if target MediaItem is found
+			JSONArray newMediaItems = new JSONArray();	// list of MediaItems minus target
+			String user = userId.getUserId(token);		// retrieve user id
+			
+			// First ensure user has a table
+			if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+				// Get user's MediaItems
+				JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+				
+				// Compose new list of MediaItems excluding item to be deleted
+				JSONObject curMediaItem;
+				for (int i = 0; i < userMediaItems.length(); i++) {
+					curMediaItem = userMediaItems.getJSONObject(i);
+					if (curMediaItem.getString(MediaItem.Field.ID.toString()).equals(mediaItemId) &&
+						curMediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+						targetFound = true;
+					} else {
+						newMediaItems.put(curMediaItem);
+					}
+				}
+			}
+			
+			// Overwrite user's MediaItems with updated list if target found
+			if (targetFound) {
+				db.getJSONObject(MEDIAITEMS_TABLE).put(user, newMediaItems);
+				return ImmediateFuture.newInstance(null);
+			} else {
+				throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem ID " + mediaItemId + " does not exist existin within Album " + albumId);
+			}
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO: not using appId
+	public Future<Void> createMediaItem(UserId userId, String appId,
+			String albumId, MediaItem mediaItem, SecurityToken token)
+			throws ProtocolException {
+		try {
+			// Get table of user's MediaItems
+			JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(userId.getUserId(token));
+			if (userMediaItems == null) {
+				userMediaItems = new JSONArray();
+				db.getJSONObject(MEDIAITEMS_TABLE).put(userId.getUserId(token), userMediaItems);
+			}
+
+			// Convert MediaItem to JSON and set ID & Album ID
+			JSONObject jsonMediaItem = convertToJson(mediaItem);
+			jsonMediaItem.put(MediaItem.Field.ALBUM_ID.toString(), albumId);
+			if (!jsonMediaItem.has(MediaItem.Field.ID.toString())) {
+				jsonMediaItem.put(MediaItem.Field.ID.toString(), System.currentTimeMillis());
+			}
+
+			// Insert new MediaItem into table
+			userMediaItems.put(jsonMediaItem);
+			return ImmediateFuture.newInstance(null);
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO: not using appId
+	public Future<Void> updateMediaItem(UserId userId, String appId,
+			String albumId, String mediaItemId, MediaItem mediaItem,
+			SecurityToken token) throws ProtocolException {
+		try {
+			// First ensure user has a table
+			String user = userId.getUserId(token);
+			if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+				// Retrieve user's MediaItems
+				JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+				
+				// Convert MediaItem to JSON and set ID & Album ID
+				JSONObject jsonMediaItem = convertToJson(mediaItem);
+				jsonMediaItem.put(MediaItem.Field.ID.toString(), mediaItemId);
+				jsonMediaItem.put(MediaItem.Field.ALBUM_ID.toString(), albumId);
+				
+				// Iterate through MediaItems to identify item to update
+				JSONObject curMediaItem = null;
+				for (int i = 0; i < userMediaItems.length(); i++) {
+					curMediaItem = userMediaItems.getJSONObject(i);
+					if (curMediaItem.getString(MediaItem.Field.ID.toString()).equals(mediaItemId) &&
+						curMediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+						userMediaItems.put(i, jsonMediaItem);
+						return ImmediateFuture.newInstance(null);
+					}
+				}
+			}
+
+			// Error - no MediaItem found with given ID and Album ID
+			throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem ID " + mediaItemId + " does not exist existin within Album " + albumId);
+		} catch (JSONException je) {
+			throw new ProtocolException(
+					HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+					je.getMessage(), je);
+		}
+	}
+
+	// TODO Why specifically handle Activity instead of generic POJO (below)?
+	private JSONObject convertFromActivity(Activity activity, Set<String> fields)
+			throws JSONException {
+		// TODO Not using fields yet
+		return new JSONObject(converter.convertToString(activity));
+	}
+
+	private JSONObject convertToJson(Object object) throws JSONException {
+		// TODO not using fields yet
+		return new JSONObject(converter.convertToString(object));
+	}
+
+	public <T> T filterFields(JSONObject object, Set<String> fields,
+			Class<T> clz) throws JSONException {
+		if (!fields.isEmpty()) {
+			// Create a copy with just the specified fields
+			object = new JSONObject(object, fields.toArray(new String[fields
+					.size()]));
+		}
+		return converter.convertToObject(object.toString(), clz);
+	}
 }