← Back to team overview

mugle-dev team mailing list archive

[Merge] lp:~mgiuca/mugle/gamefiles into lp:mugle

 

Matt Giuca has proposed merging lp:~mgiuca/mugle/gamefiles into lp:mugle.

Requested reviews:
  MUGLE Developers (mugle-dev)
Related bugs:
  Bug #786395 in MUGLE: "Blobstore API requires billing enabled on App Engine"
  https://bugs.launchpad.net/mugle/+bug/786395

For more details, see:
https://code.launchpad.net/~mgiuca/mugle/gamefiles/+merge/61896

If anyone is still up, can you have a look at this diff. It introduces game file uploading. I just want to check if I am doing the database access right -- particularly with the definition of the GameFileContents class, and the way in which GameFileData and GameFileContents objects are created and looked up.

If not, I'll just commit it.

Note that I'm not quite ready to commit; I need to add MIME type detection and game file serving, but that doesn't need serious review.
-- 
https://code.launchpad.net/~mgiuca/mugle/gamefiles/+merge/61896
Your team MUGLE Developers is requested to review the proposed merge of lp:~mgiuca/mugle/gamefiles into lp:mugle.
=== modified file 'doc/platform/urls.rst'
--- doc/platform/urls.rst	2011-05-22 08:01:47 +0000
+++ doc/platform/urls.rst	2011-05-22 15:05:59 +0000
@@ -1,120 +1,244 @@
-.. Melbourne University Game-based Learning Environment
-   Copyright (C) 2011 The University of Melbourne
-
-.. This program is free software: you can redistribute it and/or modify
-   it under the terms of the GNU General Public License as published by
-   the Free Software Foundation, either version 3 of the License, or
-   (at your option) any later version.
-
-.. This program is distributed in the hope that it will be useful,
-   but WITHOUT ANY WARRANTY; without even the implied warranty of
-   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-   GNU General Public License for more details.
-
-.. You should have received a copy of the GNU General Public License
-   along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-URL scheme
-==========
-
-MUGLE has a two-tier URL scheme, since it is primarily a client-side web
-application, and users need to be able to navigate around the site without
-reloading the page. Therefore, it has *server* URLs (URLs sent to the web
-server in a conventional HTTP request) and *client* URLs (URLs suffixed to the
-``#!`` fragment part of the URL, interpreted by the browser).
-
-By policy, these URL spaces are not distinct. All client URLs are also valid
-as server URLs. Any client URL which is given as a server URL shall be
-redirected to the appropriate client URL.
-
-For example, a URL for displaying the Unimelb logo is ``/img/unimelb.png``.
-This is a server-side only URL, as it does not correspond to a web page that
-will be visited on the client; therefore it is invalid for this URL to appear
-after the ``#!``.
-
-However, a URL for the admin page is ``#!/+admin``. This is a client-side URL,
-and you can type it into the address bar to go to that page without having to
-load HTML from the server. If you do type into the URL bar ``/+admin`` (a
-server-side URL), you will be redirected to ``#!/+admin``, and the JavaScript
-on the client will handle the URL.
-
-General guidelines
-------------------
-
-Each component of the path (slash-separated tokens) is either an *open*
-namespace (users may place arbitrarily-named objects there, which must be
-unique among that namespace) or a *closed* namespace (only system-reserved
-names are used).
-
-In a closed namespace, we define the name of all objects, so there are no
-collisions. An example of a closed namespace is the ``/img/`` space, as users
-cannot upload arbitrary content there.
-
-An open namespace usually (but not always) has one type of object in it. For
-example, within a devteam, the only objects are games, which must be uniquely
-named for all games by that devteam. But in some cases, there are multiple
-types of object that share a namespace. For example, devteams and promoted
-games share the top-level namespace.
-
-Open namespaces can also have system-reserved object names. We usually prefix
-a URL component with "+" to indicate that it is a system-reserved object name
-in an open namespace. For example, ``/+admin`` is a system-reserved URL in the
-top-level namespace. It begins with a "+" to avoid conflicts with a devteam
-called "admin" (although that particular example would be a bad idea, for
-social engineering reasons). In some cases though, we haven't gotten this
-right. For example, ``/img/`` is in the top-level namespace and doesn't begin
-with a "+".
-
-Server-only URLs
-----------------
-
-These URLs are not valid on the client side. These URLs are generally in the
-following categories:
-
-* The main web page for the site (upon which all of the Ajax runs).
-* Static content (images, CSS, JavaScript, etc).
-* Ajax APIs (both the internal and the developer API, and the special upload
-  URL).
-* Developers' game URLs
-
-::
-
-    /                   Alias for /Mugle.html
-    /Mugle.html         The main web page
-    /mugle/             Contains static JavaScript and Ajax URLs
-    /img/               Contains static images
-    /favicon.ico
-    /gwt-override.css
-    /Mugle.css
-    /MugleIE6.css
-    /<promoted-game>/+play/     Static game files for game
-    /<team>/<game>/+play/       Static game files for game
-
-Client URLs
------------
-
-These URLs are valid on both the server and the client. If given to the
-server, they redirect with a "``#!``" prefixed. On the client, they implicitly
-have that prefix.
-
-::
-
-    /                   Main MUGLE screen
-    /+admin             Main administrator panel
-        /+users         Users admin page
-        /+teams         Teams admin page
-        /+games         Games admin page
-        /+vip           Promoted games admin page
-        /+news          News admin page
-    /+vip               Promoted games page
-    /+gallery           Game gallery
-    /~<username>        A user's profile
-        /+games         Games associated with this user
-        /+teams         Teams associated with this user
-        /+edit          User edit page
-    /<team>             A devteam page (share URL space with promoted game)
-        /+edit          Team edit page
-        /<game>         A game page (unique to devteam)
-            /+edit      Game edit page
-    /<promoted-game>    A promoted game (share URL space with devteam)
-        /+edit          Game edit page
+<<<<<<< TREE
+.. Melbourne University Game-based Learning Environment
+   Copyright (C) 2011 The University of Melbourne
+
+.. This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+.. This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+.. You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+URL scheme
+==========
+
+MUGLE has a two-tier URL scheme, since it is primarily a client-side web
+application, and users need to be able to navigate around the site without
+reloading the page. Therefore, it has *server* URLs (URLs sent to the web
+server in a conventional HTTP request) and *client* URLs (URLs suffixed to the
+``#!`` fragment part of the URL, interpreted by the browser).
+
+By policy, these URL spaces are not distinct. All client URLs are also valid
+as server URLs. Any client URL which is given as a server URL shall be
+redirected to the appropriate client URL.
+
+For example, a URL for displaying the Unimelb logo is ``/img/unimelb.png``.
+This is a server-side only URL, as it does not correspond to a web page that
+will be visited on the client; therefore it is invalid for this URL to appear
+after the ``#!``.
+
+However, a URL for the admin page is ``#!/+admin``. This is a client-side URL,
+and you can type it into the address bar to go to that page without having to
+load HTML from the server. If you do type into the URL bar ``/+admin`` (a
+server-side URL), you will be redirected to ``#!/+admin``, and the JavaScript
+on the client will handle the URL.
+
+General guidelines
+------------------
+
+Each component of the path (slash-separated tokens) is either an *open*
+namespace (users may place arbitrarily-named objects there, which must be
+unique among that namespace) or a *closed* namespace (only system-reserved
+names are used).
+
+In a closed namespace, we define the name of all objects, so there are no
+collisions. An example of a closed namespace is the ``/img/`` space, as users
+cannot upload arbitrary content there.
+
+An open namespace usually (but not always) has one type of object in it. For
+example, within a devteam, the only objects are games, which must be uniquely
+named for all games by that devteam. But in some cases, there are multiple
+types of object that share a namespace. For example, devteams and promoted
+games share the top-level namespace.
+
+Open namespaces can also have system-reserved object names. We usually prefix
+a URL component with "+" to indicate that it is a system-reserved object name
+in an open namespace. For example, ``/+admin`` is a system-reserved URL in the
+top-level namespace. It begins with a "+" to avoid conflicts with a devteam
+called "admin" (although that particular example would be a bad idea, for
+social engineering reasons). In some cases though, we haven't gotten this
+right. For example, ``/img/`` is in the top-level namespace and doesn't begin
+with a "+".
+
+Server-only URLs
+----------------
+
+These URLs are not valid on the client side. These URLs are generally in the
+following categories:
+
+* The main web page for the site (upon which all of the Ajax runs).
+* Static content (images, CSS, JavaScript, etc).
+* Ajax APIs (both the internal and the developer API, and the special upload
+  URL).
+* Developers' game URLs
+
+::
+
+    /                   Alias for /Mugle.html
+    /Mugle.html         The main web page
+    /mugle/             Contains static JavaScript and Ajax URLs
+    /img/               Contains static images
+    /favicon.ico
+    /gwt-override.css
+    /Mugle.css
+    /MugleIE6.css
+    /<promoted-game>/+play/     Static game files for game
+    /<team>/<game>/+play/       Static game files for game
+
+Client URLs
+-----------
+
+These URLs are valid on both the server and the client. If given to the
+server, they redirect with a "``#!``" prefixed. On the client, they implicitly
+have that prefix.
+
+::
+
+    /                   Main MUGLE screen
+    /+admin             Main administrator panel
+        /+users         Users admin page
+        /+teams         Teams admin page
+        /+games         Games admin page
+        /+vip           Promoted games admin page
+        /+news          News admin page
+    /+vip               Promoted games page
+    /+gallery           Game gallery
+    /~<username>        A user's profile
+        /+games         Games associated with this user
+        /+teams         Teams associated with this user
+        /+edit          User edit page
+    /<team>             A devteam page (share URL space with promoted game)
+        /+edit          Team edit page
+        /<game>         A game page (unique to devteam)
+            /+edit      Game edit page
+    /<promoted-game>    A promoted game (share URL space with devteam)
+        /+edit          Game edit page
+=======
+.. Melbourne University Game-based Learning Environment
+   Copyright (C) 2011 The University of Melbourne
+
+.. This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+.. This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+.. You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+URL scheme
+==========
+
+MUGLE has a two-tier URL scheme, since it is primarily a client-side web
+application, and users need to be able to navigate around the site without
+reloading the page. Therefore, it has *server* URLs (URLs sent to the web
+server in a conventional HTTP request) and *client* URLs (URLs suffixed to the
+``#!`` fragment part of the URL, interpreted by the browser).
+
+By policy, these URL spaces are not distinct. All client URLs are also valid
+as server URLs. Any client URL which is given as a server URL shall be
+redirected to the appropriate client URL.
+
+For example, a URL for displaying the Unimelb logo is ``/img/unimelb.png``.
+This is a server-side only URL, as it does not correspond to a web page that
+will be visited on the client; therefore it is invalid for this URL to appear
+after the ``#!``.
+
+However, a URL for the admin page is ``#!/+admin``. This is a client-side URL,
+and you can type it into the address bar to go to that page without having to
+load HTML from the server. If you do type into the URL bar ``/+admin`` (a
+server-side URL), you will be redirected to ``#!/+admin``, and the JavaScript
+on the client will handle the URL.
+
+General guidelines
+------------------
+
+Each component of the path (slash-separated tokens) is either an *open*
+namespace (users may place arbitrarily-named objects there, which must be
+unique among that namespace) or a *closed* namespace (only system-reserved
+names are used).
+
+In a closed namespace, we define the name of all objects, so there are no
+collisions. An example of a closed namespace is the ``/img/`` space, as users
+cannot upload arbitrary content there.
+
+An open namespace usually (but not always) has one type of object in it. For
+example, within a devteam, the only objects are games, which must be uniquely
+named for all games by that devteam. But in some cases, there are multiple
+types of object that share a namespace. For example, devteams and promoted
+games share the top-level namespace.
+
+Open namespaces can also have system-reserved object names. We usually prefix
+a URL component with "+" to indicate that it is a system-reserved object name
+in an open namespace. For example, ``/+admin`` is a system-reserved URL in the
+top-level namespace. It begins with a "+" to avoid conflicts with a devteam
+called "admin" (although that particular example would be a bad idea, for
+social engineering reasons). In some cases though, we haven't gotten this
+right. For example, ``/img/`` is in the top-level namespace and doesn't begin
+with a "+".
+
+Server-only URLs
+----------------
+
+These URLs are not valid on the client side. These URLs are generally in the
+following categories:
+
+* The main web page for the site (upon which all of the Ajax runs).
+* Static content (images, CSS, JavaScript, etc).
+* Ajax APIs (both the internal and the developer API, and the special upload
+  URL).
+* Developers' game URLs
+
+::
+
+    /                   Alias for /Mugle.html
+    /Mugle.html         The main web page
+    /mugle/             Contains static JavaScript and Ajax URLs
+        /upload         Game upload target
+    /img/               Contains static images
+    /favicon.ico
+    /gwt-override.css
+    /Mugle.css
+    /MugleIE6.css
+    /<promoted-game>/+play/     Static game files for game
+    /<team>/<game>/+play/       Static game files for game
+
+Client URLs
+-----------
+
+These URLs are valid on both the server and the client. If given to the
+server, they redirect with a "``#!``" prefixed. On the client, they implicitly
+have that prefix.
+
+::
+
+    /                   Main MUGLE screen
+    /+admin             Main administrator panel
+        /+users         Users admin page
+        /+teams         Teams admin page
+        /+games         Games admin page
+        /+vip           Promoted games admin page
+        /+news          News admin page
+    /+vip               Promoted games page
+    /+gallery           Game gallery
+    /~<username>        A user's profile
+        /+games         Games associated with this user
+        /+teams         Teams associated with this user
+        /+edit          User edit page
+    /<team>             A devteam page (share URL space with promoted game)
+        /+edit          Team edit page
+        /<game>         A game page (unique to devteam)
+            /+edit      Game edit page
+    /<promoted-game>    A promoted game (share URL space with devteam)
+        /+edit          Game edit page
+>>>>>>> MERGE-SOURCE

=== modified file 'src/au/edu/unimelb/csse/mugle/client/ui/GameEditBuilder.java'
--- src/au/edu/unimelb/csse/mugle/client/ui/GameEditBuilder.java	2011-05-22 13:33:19 +0000
+++ src/au/edu/unimelb/csse/mugle/client/ui/GameEditBuilder.java	2011-05-22 15:05:59 +0000
@@ -16,6 +16,10 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.*;
 import com.google.gwt.user.client.ui.HTMLTable.RowFormatter;
+import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.FileUpload;
+
+import com.google.gwt.user.client.Window;
 
 /**
  * Loads the view for editing game information.
@@ -105,6 +109,7 @@
         panel.add(warning);
         panel.add(new Label("Badges"));
         panel.add(assembleAchievementsTable());
+        panel.add(assembleUploadBox(gameVersion));
 
         // Assemble namePanel
         namePanel.add(new Label("Name: "));
@@ -452,6 +457,52 @@
             }           
         });
     }
+
+    /**
+     * Creates a widget allowing the user to upload a new game in a zip file.
+     * @param version Game version to upload over the top of.
+     */
+    private Widget assembleUploadBox(GameVersion version) {
+        final FormPanel form = new FormPanel();
+        form.setAction("/mugle/upload?gameversion="
+            + Long.toString(version.getPrimaryKey()));
+
+        // File upload widget requires multipart/form-data encoding, and POST
+        // method.
+        form.setEncoding(FormPanel.ENCODING_MULTIPART);
+        form.setMethod(FormPanel.METHOD_POST);
+
+        VerticalPanel vpanel = new VerticalPanel();
+        form.setWidget(vpanel);
+        vpanel.add(new Label("Upload the game code"));
+
+        HorizontalPanel hpanel = new HorizontalPanel();
+        vpanel.add(hpanel);
+
+        // Create a FileUpload widget
+        FileUpload upload = new FileUpload();
+        upload.setName("game_upload");
+        hpanel.add(upload);
+
+        // Add a 'submit' button
+        hpanel.add(new Button("Upload", new ClickListener() {
+          public void onClick(Widget sender) {
+            form.submit();
+          }
+        }));
+
+
+        // Handle the response from the server after upload
+        form.addFormHandler(new FormHandler() {
+          public void onSubmit(FormSubmitEvent event) {}
+
+          public void onSubmitComplete(FormSubmitCompleteEvent event) {
+            Window.alert(event.getResults());
+          }
+        });
+
+        return form;
+    }
     
     /**
      * Constructs the Navigation bar down the bottom (with delete button)

=== modified file 'src/au/edu/unimelb/csse/mugle/server/DataTestServiceImpl.java'
--- src/au/edu/unimelb/csse/mugle/server/DataTestServiceImpl.java	2011-05-22 13:10:42 +0000
+++ src/au/edu/unimelb/csse/mugle/server/DataTestServiceImpl.java	2011-05-22 15:05:59 +0000
@@ -100,7 +100,6 @@
 
       gamefiles[0].setPath("index.html");
       gamefiles[0].setMimeType("text/html");
-      gamefiles[0].setBlobKey(null);        // XXX
       gamefiles[0].setGameVersionKey(gameversions[0].getPrimaryKey());
 
       GameFileServiceImpl gfs = new GameFileServiceImpl();

=== modified file 'src/au/edu/unimelb/csse/mugle/server/GameFileServer.java'
--- src/au/edu/unimelb/csse/mugle/server/GameFileServer.java	2011-05-20 06:35:39 +0000
+++ src/au/edu/unimelb/csse/mugle/server/GameFileServer.java	2011-05-22 15:05:59 +0000
@@ -28,15 +28,12 @@
 import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameNotExists;
 import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameVersionNotExists;
 
-import com.google.appengine.api.blobstore.BlobKey;
-import com.google.appengine.api.blobstore.BlobstoreService;
-import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
-
 @SuppressWarnings("serial")
 public class GameFileServer extends HttpServlet {
     
     public void doGet(HttpServletRequest req, HttpServletResponse res) 
         throws IOException {
+    /*
         BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
         
         try {
@@ -45,8 +42,10 @@
         } catch (GameFileNotExists e) {
             //TODO: Construct a page with the error
         }
+    */
     }
 
+    /*
     private BlobKey getBlobKey(HttpServletRequest req) 
         throws GameFileNotExists {
         
@@ -57,7 +56,7 @@
         /* We need to parse the request to get the gameName, gameVersion
          * and path,
          * URLs are in the form /game/gameName/gameVersion/path
-         */
+         * /
         String toParse = req.getRequestURI().split("/game/")[1];  // gets everything after /game/ in the URI
         String[] splitted = toParse.split("/");
         gameName = splitted[0];
@@ -74,6 +73,7 @@
         }
         
     }
+    */
 
     /*
     public void doPost(HttpServletRequest req, HttpServletResponse res) 

=== added file 'src/au/edu/unimelb/csse/mugle/server/UploadService.java'
--- src/au/edu/unimelb/csse/mugle/server/UploadService.java	1970-01-01 00:00:00 +0000
+++ src/au/edu/unimelb/csse/mugle/server/UploadService.java	2011-05-22 15:05:59 +0000
@@ -0,0 +1,171 @@
+/*  Melbourne University Game-based Learning Environment
+ *  Copyright (C) 2011 The University of Melbourne
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package au.edu.unimelb.csse.mugle.server;
+
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.IOException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.jdo.PersistenceManager;
+
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipException;
+
+import org.apache.commons.fileupload.FileItemIterator;
+import org.apache.commons.fileupload.FileItemStream;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.fileupload.FileUploadException;
+
+import java.util.Vector;
+
+import au.edu.unimelb.csse.mugle.server.model.GameFileData;
+import au.edu.unimelb.csse.mugle.server.model.GameFileContents;
+import au.edu.unimelb.csse.mugle.server.model.GameFileGetter;
+import au.edu.unimelb.csse.mugle.server.model.GameVersionData;
+import au.edu.unimelb.csse.mugle.server.PMF;
+import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameFileNotExists;
+
+public class UploadService extends HttpServlet
+{
+    public void doPost(HttpServletRequest request,
+        HttpServletResponse response) throws IOException
+    {
+        long gameversionID = Long.parseLong(
+                                request.getParameter("gameversion"));
+        boolean got_zip = false;
+        int numfiles = 0;
+        Vector<String> filenames = new Vector<String>();
+
+        // Read the POST data fields
+        ServletFileUpload upload = new ServletFileUpload();
+        PersistenceManager pm = PMF.getManager();
+        try
+        {
+            // Get the GameVersion object
+            GameVersionData gameversion =
+                pm.getObjectById(GameVersionData.class, gameversionID);
+            // For each field
+            FileItemIterator iter = upload.getItemIterator(request);
+            while (iter.hasNext())
+            {
+                FileItemStream item = iter.next();
+                // Only care about the game_upload field
+                if (!item.getFieldName().equals("game_upload"))
+                    continue;
+                got_zip = true;
+
+                // Assume it is a ZIP file. Read each file contained inside.
+                InputStream stream = item.openStream();
+                ZipInputStream zstream = new ZipInputStream(stream);
+                ZipEntry entry;
+                while ((entry = zstream.getNextEntry()) != null)
+                {
+                    if (entry.isDirectory())
+                    {
+                        // Ignore directories (since we are not building a
+                        // tree; just remembering the path to each file)
+                        continue;
+                    }
+                    String filename = entry.getName();
+                    long size = entry.getSize();
+                    // Create a new GameFile object
+                    createGameFile(pm, gameversion, filename, zstream, size);
+                    filenames.add(filename);
+                    numfiles++;
+                }
+            }
+        }
+        catch (FileUploadException e)
+        {
+            response.sendError(400, e.toString());
+            return;
+        }
+        catch (ZipException e)
+        {
+            response.sendError(400, e.toString());
+            return;
+        }
+        finally
+        {
+            pm.close();
+        }
+        if (!got_zip)
+        {
+            response.sendError(400, "Missing POST field game_upload");
+            return;
+        }
+
+        response.setContentType("text/html");
+        PrintWriter out = response.getWriter();
+        out.println("<html>\n");
+        out.println("<p>Upload to GameVersion key " +
+            Long.toString(gameversionID) + "</p>\n");
+        out.println("<p>Number of files in ZIP: " +
+            Integer.toString(numfiles) + "</p>\n");
+        out.println("<p>Files in ZIP: " +
+            filenames.toString() + "</p>\n");
+        out.println("</html>\n");
+    }
+
+    /** Create a new GameFile and store it in the database, from a part of a
+     * Zip file.
+     * @param pm Database persistence manager.
+     * @param gameVersionKey The game version to upload this file to.
+     * @param filePath Path to the file relative to this game version.
+     * @param data Binary stream to read file contents from.
+     * @param dataSize Number of bytes to read from data.
+     */
+    public static void createGameFile(PersistenceManager pm,
+        GameVersionData gameVersion, String filePath, InputStream data,
+        long dataSize)
+        throws IOException
+    {
+        // Check if a file already exists at that path
+        GameFileData file;
+        GameFileContents contents;
+        try
+        {
+            file = GameFileGetter.getGameFile(pm, gameVersion, filePath);
+        }
+        catch (GameFileNotExists e)
+        {
+            // First, store the contents as a blob in the database
+            contents = new GameFileContents(data, dataSize);
+            pm.makePersistent(contents);
+            // Then, create a GameFile which points at that blob
+            file = new GameFileData(gameVersion, filePath, contents);
+            pm.makePersistent(file);
+            return;
+        }
+        // Already exists; just update the file and contents
+        // (Note that if we were uploading a new version, we wouldn't
+        // overwrite the file or contents, but since we're updating an
+        // existing version, we can replace the contents).
+        // XXX Note that if contents are shared across versions this will
+        // clobber ALL versions, so probably we should create a new content
+        // object each time.
+        contents = pm.getObjectById(GameFileContents.class,
+                                    file.getContents());
+        contents.setContents(data, dataSize);
+        // Also set the MIME type based on the filename (so the result is
+        // consistent with what would have happened if the file didn't exist).
+        file.setMimeTypeFromFilename();
+    }
+}

=== added file 'src/au/edu/unimelb/csse/mugle/server/model/GameFileContents.java'
--- src/au/edu/unimelb/csse/mugle/server/model/GameFileContents.java	1970-01-01 00:00:00 +0000
+++ src/au/edu/unimelb/csse/mugle/server/model/GameFileContents.java	2011-05-22 15:05:59 +0000
@@ -0,0 +1,85 @@
+/*  Melbourne University Game-based Learning Environment
+ *  Copyright (C) 2011 The University of Melbourne
+ *
+ *  This program is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation, either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package au.edu.unimelb.csse.mugle.server.model;
+
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+
+import javax.jdo.PersistenceManager;
+import javax.jdo.annotations.IdGeneratorStrategy;
+import javax.jdo.annotations.PersistenceCapable;
+import javax.jdo.annotations.Persistent;
+import javax.jdo.annotations.PrimaryKey;
+
+import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.Blob;
+
+/** The contents of a GameFile.
+ * This simple class contains only the contents and no other meta-data. It
+ * may be referenced by one or more GameFile objects.
+ */
+@PersistenceCapable
+public class GameFileContents {
+    // Fields
+
+    @PrimaryKey
+    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
+    private Key id;
+
+    @Persistent
+    private Blob data;
+
+    /** Create a new file contents blob, from a given input stream.
+     @param size The number of bytes to read from stream.
+     */
+    public GameFileContents(InputStream stream, long size)
+        throws IOException
+    {
+        this.setContents(stream, size);
+    }
+
+    // Getters
+
+    public Key getServerKey() {
+        return this.id;
+    }
+
+    // Misc methods
+
+    /** Write the contents of the file to the OutputStream object.
+     * @param writer Stream object to receive the contents of the file.
+     */
+    public void getContents(OutputStream writer) {
+        //TODO: retrieve file from database
+    }
+
+    /** Read the contents of an InputStream and store it in the database as
+     * the contents of this file.
+     * @param stream Stream object to read contents of file from.
+     @param size The number of bytes to read from stream.
+     */
+    public void setContents(InputStream stream, long size)
+        throws IOException
+    {
+        //TODO: check that the DevTeam's space limit hasn't been exceeded.
+        byte[] bytes = new byte[(int)size];
+        stream.read(bytes, 0, (int)size);
+        this.data = new Blob(bytes);
+    }
+}

=== modified file 'src/au/edu/unimelb/csse/mugle/server/model/GameFileData.java'
--- src/au/edu/unimelb/csse/mugle/server/model/GameFileData.java	2011-05-22 11:06:46 +0000
+++ src/au/edu/unimelb/csse/mugle/server/model/GameFileData.java	2011-05-22 15:05:59 +0000
@@ -25,8 +25,8 @@
 import javax.jdo.annotations.Persistent;
 import javax.jdo.annotations.PrimaryKey;
 
-import com.google.appengine.api.blobstore.BlobKey;
 import com.google.appengine.api.datastore.Key;
+import com.google.appengine.api.datastore.Blob;
 
 import au.edu.unimelb.csse.mugle.shared.model.*;
 import au.edu.unimelb.csse.mugle.server.PMF;
@@ -50,7 +50,7 @@
     private String mimeType; //mime type of the file
 
     @Persistent
-    private BlobKey blobKey; //key to the actual blob -- may be shared between GameFiles across versions
+    private Key contents; //key to the GameFileContents
 
     @Persistent
     private Key version; //Game version this Gamefile belongs to
@@ -72,23 +72,43 @@
     public GameFileData() {
         this.version = null;
         this.setMimeType(null);
-        this.setBlobKey(null);
         this.path = null;
     }
 
-/** TODO: Pending Blob implementation for files
-    // For creation of the file for the first time
-    public GameFile (String path, GameVersion version, InputStream contents) {
-        this.path = path;
-        this.versions.add(version.getPrimaryKey());
-        this.setContents(contents);
-    }
-*/
+    /** Create a new GameFileData object.
+     * Guesses the file's MIME type based on the file extension.
+     * @param version The game version the file belongs to.
+     * @param path Path to the file relative to this gameversion.
+     * @param contents Blob containing the file's contents.
+     */
+    public GameFileData(GameVersionData version, String path,
+        GameFileContents contents)
+    {
+        this.version = version.getServerKey();
+        this.path = path;
+        this.setMimeTypeFromFilename();
+        this.contents = contents.getServerKey();
+    }
+
+    /** Create a new GameFileData object.
+     * @param version The game version the file belongs to.
+     * @param path Path to the file relative to this gameversion.
+     * @param mimeType MIME type of the file.
+     * @param contents Blob containing the file's contents.
+     */
+    public GameFileData(GameVersionData version, String path, String mimeType,
+        GameFileContents contents)
+    {
+        this.version = version.getServerKey();
+        this.path = path;
+        this.mimeType = mimeType;
+        this.contents = contents.getServerKey();
+    }
 
     /* When a file is moved, or renamed - The existing GameFile entry should be deleted,
      * in which case, we may want to initialise with multiple Game versions.
      */
-/**  TODO: Pending Blob implementation for files
+/**  TODO: Pending multiple versions of games
     public GameFile (String path, Set<String> versions, InputStream contents) {
         this.path = path;
         this.versions = versions;
@@ -122,6 +142,7 @@
         return mimeType;
     }
 
+<<<<<<< TREE
     @GetterUserLevel(privateView=Role.GUEST, publicView=Role.GUEST, mappedTo="setBlobKey", overrideBy="getBlobKeyString")
     public BlobKey getBlobKey() {
         return this.blobKey;
@@ -134,6 +155,8 @@
         return this.blobKey.getKeyString();
     }
 
+=======
+>>>>>>> MERGE-SOURCE
     @Override
     @GetterUserLevel(privateView=Role.GUEST, publicView=Role.GUEST, mappedTo="setCreatedUser", overrideBy="keyToCreated")
     public Key getCreatedUser() {
@@ -209,40 +232,27 @@
         this.mimeType = mimeType;
     }
 
-    // TODO: do we allow the clients to modify this value
-    //@SetterUserLevel(getter="", mappedBy="")
-    public void setBlobKey(BlobKey blobKey) {
-        this.blobKey = blobKey;
-    }
-
     // Misc methods
 
-    /** Write the contents of the file to the PrintWriter object.
-     * @param writer Stream object to receive the contents of the file.
-     */
-/**  TODO: Pending Blob implementation for files
-    public void getContents(PrintWriter writer) {
-        //TODO: retrieve file from database
-    }
-*/
-
-    /** Read the contents of an InputStream and store it in the database as
-     * the contents of this file.
-     * @param stream Stream object to read contents of file from.
-     */
-/**  TODO: Pending Blob implementation for files
-    public void setContents(InputStream stream) {
-        //TODO: add/create the file in the database
-        //TODO: check that the DevTeam's space limit hasn't been exceeded.
-    }
-*/
-
-
-/**  TODO: Pending Blob implementation for files
-    public GameFileData(String path, GameVersion version, InputStream contents) {
-        super(path, version, contents);
-    }
-*/
+    /** Get the content blob of this file.
+     * @return Key of the GameFileContents object.
+     */
+    public Key getContents() {
+        return this.contents;
+    }
+
+    /** Set the content blob of this file.
+     * @param contents Key of the GameFileContents object.
+     */
+    public void setContents(Key contents) {
+        this.contents = contents;
+    }
+
+    /** Set the MIME type based on the file extension.
+     */
+    public void setMimeTypeFromFilename() {
+        // TODO Get MIME type based on extension
+    }
 
     @Override
     public Key getServerKey() {

=== modified file 'src/au/edu/unimelb/csse/mugle/server/model/GameFileGetter.java'
--- src/au/edu/unimelb/csse/mugle/server/model/GameFileGetter.java	2011-05-22 11:06:46 +0000
+++ src/au/edu/unimelb/csse/mugle/server/model/GameFileGetter.java	2011-05-22 15:05:59 +0000
@@ -167,6 +167,33 @@
     }
     
     /**
+     * Gets the GameFile by its path and version, for editing object in
+     * datastore.
+     * PersistenceManager must be handled by the caller
+     * @para pm The Persistence Manager
+     * @param gameVersion the version of the game
+     * @param path the path of the file
+     * @return the GameFile - read only
+     * @throws GameFileNotExists
+     */
+    @SuppressWarnings("unchecked")
+    public static GameFileData getGameFile(PersistenceManager pm,
+        GameVersionData gameVersion, String path)
+        throws GameFileNotExists {
+
+        Query q = pm.newQuery(GameFileData.class, "path == p && version == v");
+        q.declareParameters("String p, com.google.appengine.api.datastore.Key v");
+        List<GameFileData> results = (List<GameFileData>) q.execute(path,
+            gameVersion.getServerKey());
+
+        if (results.isEmpty()) {
+            throw new GameFileNotExists(String.valueOf(gameVersion), path);
+        }
+
+        return results.get(0);
+    }
+
+    /**
      * Gets the GameFile associated with the given GameFileData primary key.
      * @param pm The PersistenceManager
      * @param key The server-side key (ModelDataClass.getServerKey())

=== modified file 'src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileNotExists.java'
--- src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileNotExists.java	2011-05-20 06:35:39 +0000
+++ src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileNotExists.java	2011-05-22 15:05:59 +0000
@@ -16,4 +16,9 @@
         super("The file " + path + " for the version " + gameVersion + 
                         " of " + gameName + " does not Exist.");
     }
+
+    public GameFileNotExists(String gameVersion, String path) {
+        super("The file " + path + " for the version " + gameVersion +
+                        " of <unknown game> does not Exist.");
+    }
 }

=== added file 'war/WEB-INF/lib/commons-fileupload-1.2.2.jar'
Binary files war/WEB-INF/lib/commons-fileupload-1.2.2.jar	1970-01-01 00:00:00 +0000 and war/WEB-INF/lib/commons-fileupload-1.2.2.jar	2011-05-22 15:05:59 +0000 differ
=== modified file 'war/WEB-INF/web.xml'
--- war/WEB-INF/web.xml	2011-05-08 06:05:33 +0000
+++ war/WEB-INF/web.xml	2011-05-22 15:05:59 +0000
@@ -132,6 +132,18 @@
     <servlet-name>UserGameProfileService</servlet-name>
     <url-pattern>/mugle/data-ugp</url-pattern>
   </servlet-mapping>
+
+  <!-- Special Upload Service (not AJAX) -->
+
+  <servlet>
+    <servlet-name>UploadService</servlet-name>
+    <servlet-class>au.edu.unimelb.csse.mugle.server.UploadService</servlet-class>
+  </servlet>
+
+  <servlet-mapping>
+    <servlet-name>UploadService</servlet-name>
+    <url-pattern>/mugle/upload</url-pattern>
+  </servlet-mapping>
   
   <!-- API servlet mappings -->
   


Follow ups