Hi, I'm implementing Content Upload<http://opensocial-resources.googlecode.com/svn/spec/1.0/Core-API-Server.xml#Content-Upload>spec and I have a few question to be sure that I'm understanding it correctly:
I wrote an extend exaplantion in a post with title "Uploading and storing files data", but it can be summarized in that Uploaded Content is a param for some other action. I mean, if we want to create an Activity that contains an image, the we call the create activity service with the usual params of the activity and also with a file as a param. Uploading a file param is not easy, so this spec is about how to handle that. If that interpretation is right, then uploading content is an extension on what type of params can be used to create/update any entity. Therefore we need a general way to handle files that could be used by potentially any service. So, I'm right now trying to implement the RPC part of the spec, since it can be used from the browser and all the multipart-form handling code is already there in Shindig. Just to start with something I've modified AppDataHandler to handle binary files (I know that the spec says that App Data only handles Strings values, but I need something like for myself, I'll port this to meet the spec). So now the AppDataHandler.create(...) method is able to handle files if they are in the SocialRequestItem. It saves the file using a new service ContentUploadService. Then replace the key that is mapped to that file, e.g: If the there is a values map with: "data" : { "image":"@field:image1" } Then it searches for a FormMimePart with name "image1", saves the content to a new file, gets this file's ID and changes that value with it: "data" : { "image":"<fileID>" } Then continues as before. I'm not sure if that's the best place, I mean that Handles handle the file storage and then resolving the reference to the file. The good side of this is that each handler would be able to decide what to do with file params. The bad side is that it could be resolved when the raw content of the request is being converted, so it would be transparent to handlers. Any suggestion on this? Another thing is that I'm not sure about how to get that file, I mean to see it in the browser, AFAIU the file ID could be actually an URL that can be used to get the file. Is this right? So WDYT? I'm on the right way? BTW, I'm attaching a patch with the changes. Thanks, Gabriel
### Eclipse Workspace Patch 1.0 #P shindig-social-api Index: src/main/java/org/apache/shindig/social/opensocial/service/AppDataHandler.java =================================================================== --- src/main/java/org/apache/shindig/social/opensocial/service/AppDataHandler.java (revision 963850) +++ src/main/java/org/apache/shindig/social/opensocial/service/AppDataHandler.java (working copy) @@ -17,29 +17,36 @@ */ package org.apache.shindig.social.opensocial.service; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; import org.apache.shindig.protocol.HandlerPreconditions; import org.apache.shindig.protocol.Operation; import org.apache.shindig.protocol.ProtocolException; import org.apache.shindig.protocol.Service; +import org.apache.shindig.protocol.multipart.FormDataItem; import org.apache.shindig.social.opensocial.spi.AppDataService; +import org.apache.shindig.social.opensocial.spi.ContentUploadService; import org.apache.shindig.social.opensocial.spi.UserId; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Future; - -import javax.servlet.http.HttpServletResponse; - import com.google.inject.Inject; @Service(name = "appdata", path = "/{userId}+/{groupId}/{appId}") public class AppDataHandler { private final AppDataService service; + private final ContentUploadService contentService; @Inject - public AppDataHandler(AppDataService service) { + public AppDataHandler(AppDataService service, ContentUploadService contentService) { this.service = service; + this.contentService = contentService; } /** @@ -103,6 +110,38 @@ } } + // Some entries could have a reference to a file that needs to be saved. + for (Map.Entry<String, String> entry : values.entrySet()) { + if (entry.getValue() instanceof String) { + + String value = entry.getValue(); + + // If the value is a reference to another field + if (value.startsWith("@field:")) { + String fieldName = value.substring("@field:".length()); + FormDataItem dataItem = request.getFormMimePart(fieldName); + + // It could be a reference to a file being uploaded + if (dataItem != null && !dataItem.isFormField()) { + try { + // If so, save the file and change the reference to the actual file ID + Future<String> fileId = contentService.write(IOUtils.toByteArray(dataItem.getInputStream())); + values.put(entry.getKey(), fileId.get()); + } catch (IOException e) { + throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Error uploading file in field: " + fieldName); + } catch (InterruptedException e) { + throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Error uploading file in field: " + fieldName); + } catch (ExecutionException e) { + throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + "Error uploading file in field: " + fieldName); + } + } + } + } + } + return service.updatePersonData(userIds.iterator().next(), request.getGroup(), request.getAppId(), request.getFields(), values, request.getToken()); } Index: src/main/java/org/apache/shindig/social/sample/SampleModule.java =================================================================== --- src/main/java/org/apache/shindig/social/sample/SampleModule.java (revision 963850) +++ src/main/java/org/apache/shindig/social/sample/SampleModule.java (working copy) @@ -20,9 +20,11 @@ import org.apache.shindig.social.opensocial.oauth.OAuthDataStore; import org.apache.shindig.social.opensocial.spi.ActivityService; import org.apache.shindig.social.opensocial.spi.AppDataService; +import org.apache.shindig.social.opensocial.spi.ContentUploadService; import org.apache.shindig.social.opensocial.spi.MessageService; import org.apache.shindig.social.opensocial.spi.PersonService; import org.apache.shindig.social.sample.oauth.SampleOAuthDataStore; +import org.apache.shindig.social.sample.spi.FileDbService; import org.apache.shindig.social.sample.spi.JsonDbOpensocialService; import com.google.inject.AbstractModule; @@ -44,6 +46,7 @@ bind(AppDataService.class).to(JsonDbOpensocialService.class); bind(PersonService.class).to(JsonDbOpensocialService.class); bind(MessageService.class).to(JsonDbOpensocialService.class); + bind(ContentUploadService.class).to(FileDbService.class); bind(OAuthDataStore.class).to(SampleOAuthDataStore.class); } Index: src/main/java/org/apache/shindig/social/sample/spi/FileDbService.java =================================================================== --- src/main/java/org/apache/shindig/social/sample/spi/FileDbService.java (revision 0) +++ src/main/java/org/apache/shindig/social/sample/spi/FileDbService.java (revision 0) @@ -0,0 +1,68 @@ +package org.apache.shindig.social.sample.spi; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.Future; + +import org.apache.commons.io.IOUtils; +import org.apache.shindig.common.util.ImmediateFuture; +import org.apache.shindig.social.opensocial.spi.ContentUploadService; + +import com.google.common.collect.Maps; + +//TODO-GG concurrency +public class FileDbService implements ContentUploadService { + + private Map<String, String> fileIdNameMap; + private String tmpDir; + + public FileDbService() { + fileIdNameMap = Maps.newHashMap(); + //TODO-GG real java tmp dir, or a fixed one? + tmpDir = "/tmp/"; + } + + public Future<byte[]> read(String fileId) { + FileInputStream fis = null; + + try { + fis = new FileInputStream(fileIdNameMap.get(fileId)); + return ImmediateFuture.newInstance(IOUtils.toByteArray(fis)); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return null; + } + + public Future<String> write(byte[] data) { + FileOutputStream fos = null; + String fileId = null; + try { + fileId = getNextFileId(); + String fileName = tmpDir + fileId; + fos = new FileOutputStream(fileName); + IOUtils.write(data, fos); + fileIdNameMap.put(fileId, fileName); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(fos); + } + return ImmediateFuture.newInstance(fileId); + } + + private String getNextFileId() { + return "file-" + fileIdNameMap.size(); + } +} Index: src/main/java/org/apache/shindig/social/opensocial/spi/ContentUploadService.java =================================================================== --- src/main/java/org/apache/shindig/social/opensocial/spi/ContentUploadService.java (revision 0) +++ src/main/java/org/apache/shindig/social/opensocial/spi/ContentUploadService.java (revision 0) @@ -0,0 +1,10 @@ +package org.apache.shindig.social.opensocial.spi; + +import java.util.concurrent.Future; + +public interface ContentUploadService { + + Future<String> write(byte[] data); + + Future<byte[]> read(String fileId); +}