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

Proposed by Matt Giuca
Status: Merged
Merged at revision: 339
Proposed branch: lp:~mgiuca/mugle/gamefiles
Merge into: lp:mugle
Diff against target: 1076 lines (+688/-96)
15 files modified
doc/platform/urls.rst (+3/-2)
src/META-INF/mime.types (+35/-0)
src/au/edu/unimelb/csse/mugle/client/ui/GameEditBuilder.java (+54/-0)
src/au/edu/unimelb/csse/mugle/server/DataTestServiceImpl.java (+0/-1)
src/au/edu/unimelb/csse/mugle/server/GameFileServer.java (+69/-13)
src/au/edu/unimelb/csse/mugle/server/UploadService.java (+197/-0)
src/au/edu/unimelb/csse/mugle/server/model/GameFileContents.java (+104/-0)
src/au/edu/unimelb/csse/mugle/server/model/GameFileData.java (+55/-56)
src/au/edu/unimelb/csse/mugle/server/model/GameFileGetter.java (+89/-4)
src/au/edu/unimelb/csse/mugle/server/model/GameGetter.java (+14/-9)
src/au/edu/unimelb/csse/mugle/server/model/GameVersionGetter.java (+19/-9)
src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileContentsNotExists.java (+15/-0)
src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileNotExists.java (+7/-2)
src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameNotExists.java (+5/-0)
war/WEB-INF/web.xml (+22/-0)
To merge this branch: bzr merge lp:~mgiuca/mugle/gamefiles
Reviewer Review Type Date Requested Status
MUGLE Developers Pending
Review via email: mp+61896@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
lp:~mgiuca/mugle/gamefiles updated
324. By Matt Giuca

Merge from trunk.

325. By Matt Giuca

UploadService: As Scott recommends, open a new PersistenceManager for each file.

326. By Matt Giuca

GameFileData: Now correctly sets the MIME type of a file based on its extension.
Added src/META-INF/mime.types, a table mapping file extensions to MIME types (manually created).

327. By Matt Giuca

web.xml: Set up URL handler for GameFileServer.
Unfortunately, had to change URL pattern to /+games/* since I can't match /*/*/+play in web.xml, as originally planned.

328. By Matt Giuca

GameFileServer: Started handling URL.

329. By Matt Giuca

GameFileServer: Clean up debugging output.

330. By Matt Giuca

GameVersionGetter: Fix getGameVersion by Key double-closing the persistence manager.

331. By Matt Giuca

GameVersionGetter: More useful methods.

332. By Matt Giuca

GameGetter: getGameByDevTeam now actually takes a game name.
What ... was it doing before? It seemed to be expecting that each devteam had only one game, and didn't check the name.

333. By Matt Giuca

GameFileServer: Now gets the gameversion associated with the URL, and throws errors if they are missing.

334. By Matt Giuca

GameFileGetter: More methods.

335. By Matt Giuca

Fix GameFileGetter error messages.

336. By Matt Giuca

GameFileGetter: Can now get GameFileContents from a GameFile.
Added new exception type GameFileContentsNotExists.

337. By Matt Giuca

UploadService now uses GameFileGetter.getGameFileContents.

338. By Matt Giuca

GameFileServer now gets the game file and contents specified.

339. By Matt Giuca

GameFileContents: getContents now works.

340. By Matt Giuca

GameFileServer: Prints the MIME type and contents to the debugging output.

341. By Matt Giuca

GameFileServer: Replaced debug output with actual file service.
Now serves the raw bytes of the file with the appropriate MIME type.
Looks good ... up until around 0x200 bytes where it becomes all 0s.

342. By Matt Giuca

GameFileServer: Now sets Content-Length for better browser handling.

343. By Matt Giuca

GameFileContents: Fix bug where unzipping files beyond a certain size would truncate the file with null bytes.
It needs to read in a loop, because stream.read won't necessarily return all the bytes at once.

344. By Matt Giuca

UploadService: Fix bug where uploading a new file over an old one would not replace the contents.
For some reason you have to call pm.makePersistent on contents. I thought you didn't need to if the object already existed?
(It only is for this object; the other ones you don't need to call makePersistent to make a change. Strange.)

345. By Matt Giuca

UploadService: Change result so it's no longer debug text.
It shows the list of filenames -- it'll do for now as a user interface.

346. By Matt Giuca

UploadService: Remove the first path segment from the filename, so the top-level directory of the zip file is ignored.

347. By Matt Giuca

GameEditBuilder: Added instructions for uploading a zip file.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'doc/platform/urls.rst'
2--- doc/platform/urls.rst 2011-05-22 08:01:47 +0000
3+++ doc/platform/urls.rst 2011-05-22 18:34:23 +0000
4@@ -82,13 +82,14 @@
5 / Alias for /Mugle.html
6 /Mugle.html The main web page
7 /mugle/ Contains static JavaScript and Ajax URLs
8+ /upload Game upload target
9 /img/ Contains static images
10 /favicon.ico
11 /gwt-override.css
12 /Mugle.css
13 /MugleIE6.css
14- /<promoted-game>/+play/ Static game files for game
15- /<team>/<game>/+play/ Static game files for game
16+ /+games/<promoted-game>/ Static game files for game
17+ /+games/<team>/<game>/ Static game files for game
18
19 Client URLs
20 -----------
21
22=== added file 'src/META-INF/mime.types'
23--- src/META-INF/mime.types 1970-01-01 00:00:00 +0000
24+++ src/META-INF/mime.types 2011-05-22 18:34:23 +0000
25@@ -0,0 +1,35 @@
26+# MUGLE MIME types mapping
27+# Used to set the MIME type of files being uploaded
28+# This list should be just about all the file types students will need.
29+# the format is <mime type> <space separated file extensions>
30+
31+# Application
32+application/json json
33+application/javascript js
34+application/pdf pdf
35+application/xhtml+xml xhtml
36+application/zip zip
37+application/x-gzip gz gzip
38+
39+# Text
40+text/plain txt text
41+text/html htm html
42+text/css css
43+text/xml xml
44+
45+# Image
46+image/gif gif
47+image/jpeg jpg jpeg
48+image/png png
49+image/svg svg
50+image/webp webp
51+
52+# Audio
53+audio/mpeg mp3
54+audio/ogg ogg
55+audio/vnd.wave wav
56+
57+# Video
58+video/mpeg mpeg
59+video/mp4 mp4
60+video/webm webm
61
62=== modified file 'src/au/edu/unimelb/csse/mugle/client/ui/GameEditBuilder.java'
63--- src/au/edu/unimelb/csse/mugle/client/ui/GameEditBuilder.java 2011-05-22 13:33:19 +0000
64+++ src/au/edu/unimelb/csse/mugle/client/ui/GameEditBuilder.java 2011-05-22 18:34:23 +0000
65@@ -16,6 +16,10 @@
66 import com.google.gwt.user.client.rpc.AsyncCallback;
67 import com.google.gwt.user.client.ui.*;
68 import com.google.gwt.user.client.ui.HTMLTable.RowFormatter;
69+import com.google.gwt.user.client.ui.FormPanel;
70+import com.google.gwt.user.client.ui.FileUpload;
71+
72+import com.google.gwt.user.client.Window;
73
74 /**
75 * Loads the view for editing game information.
76@@ -105,6 +109,7 @@
77 panel.add(warning);
78 panel.add(new Label("Badges"));
79 panel.add(assembleAchievementsTable());
80+ panel.add(assembleUploadBox(gameVersion));
81
82 // Assemble namePanel
83 namePanel.add(new Label("Name: "));
84@@ -452,6 +457,55 @@
85 }
86 });
87 }
88+
89+ /**
90+ * Creates a widget allowing the user to upload a new game in a zip file.
91+ * @param version Game version to upload over the top of.
92+ */
93+ private Widget assembleUploadBox(GameVersion version) {
94+ final FormPanel form = new FormPanel();
95+ form.setAction("/mugle/upload?gameversion="
96+ + Long.toString(version.getPrimaryKey()));
97+
98+ // File upload widget requires multipart/form-data encoding, and POST
99+ // method.
100+ form.setEncoding(FormPanel.ENCODING_MULTIPART);
101+ form.setMethod(FormPanel.METHOD_POST);
102+
103+ VerticalPanel vpanel = new VerticalPanel();
104+ form.setWidget(vpanel);
105+ vpanel.add(new Label("Upload the game code. Instructions: Compile "
106+ + "your GWT project and then zip up the 'war' directory. Your zip "
107+ + "file must have a single top-level directory. Upload the zip file "
108+ + "here. This will replace any existing versions of the game."));
109+
110+ HorizontalPanel hpanel = new HorizontalPanel();
111+ vpanel.add(hpanel);
112+
113+ // Create a FileUpload widget
114+ FileUpload upload = new FileUpload();
115+ upload.setName("game_upload");
116+ hpanel.add(upload);
117+
118+ // Add a 'submit' button
119+ hpanel.add(new Button("Upload", new ClickListener() {
120+ public void onClick(Widget sender) {
121+ form.submit();
122+ }
123+ }));
124+
125+
126+ // Handle the response from the server after upload
127+ form.addFormHandler(new FormHandler() {
128+ public void onSubmit(FormSubmitEvent event) {}
129+
130+ public void onSubmitComplete(FormSubmitCompleteEvent event) {
131+ Window.alert(event.getResults());
132+ }
133+ });
134+
135+ return form;
136+ }
137
138 /**
139 * Constructs the Navigation bar down the bottom (with delete button)
140
141=== modified file 'src/au/edu/unimelb/csse/mugle/server/DataTestServiceImpl.java'
142--- src/au/edu/unimelb/csse/mugle/server/DataTestServiceImpl.java 2011-05-22 13:10:42 +0000
143+++ src/au/edu/unimelb/csse/mugle/server/DataTestServiceImpl.java 2011-05-22 18:34:23 +0000
144@@ -100,7 +100,6 @@
145
146 gamefiles[0].setPath("index.html");
147 gamefiles[0].setMimeType("text/html");
148- gamefiles[0].setBlobKey(null); // XXX
149 gamefiles[0].setGameVersionKey(gameversions[0].getPrimaryKey());
150
151 GameFileServiceImpl gfs = new GameFileServiceImpl();
152
153=== modified file 'src/au/edu/unimelb/csse/mugle/server/GameFileServer.java'
154--- src/au/edu/unimelb/csse/mugle/server/GameFileServer.java 2011-05-20 06:35:39 +0000
155+++ src/au/edu/unimelb/csse/mugle/server/GameFileServer.java 2011-05-22 18:34:23 +0000
156@@ -16,37 +16,92 @@
157 */
158 package au.edu.unimelb.csse.mugle.server;
159
160+import java.io.OutputStream;
161 import java.io.IOException;
162
163 import javax.servlet.http.HttpServlet;
164 import javax.servlet.http.HttpServletRequest;
165 import javax.servlet.http.HttpServletResponse;
166
167+import com.google.appengine.api.datastore.Key;
168+
169+import au.edu.unimelb.csse.mugle.server.model.GameData;
170+import au.edu.unimelb.csse.mugle.server.model.GameGetter;
171+import au.edu.unimelb.csse.mugle.server.model.GameVersionData;
172+import au.edu.unimelb.csse.mugle.server.model.GameVersionGetter;
173 import au.edu.unimelb.csse.mugle.server.model.GameFileData;
174 import au.edu.unimelb.csse.mugle.server.model.GameFileGetter;
175+import au.edu.unimelb.csse.mugle.server.model.GameFileContents;
176+import au.edu.unimelb.csse.mugle.shared.platform.exceptions.DevTeamNotExists;
177 import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameFileNotExists;
178+import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameFileContentsNotExists;
179 import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameNotExists;
180 import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameVersionNotExists;
181
182-import com.google.appengine.api.blobstore.BlobKey;
183-import com.google.appengine.api.blobstore.BlobstoreService;
184-import com.google.appengine.api.blobstore.BlobstoreServiceFactory;
185-
186 @SuppressWarnings("serial")
187 public class GameFileServer extends HttpServlet {
188
189 public void doGet(HttpServletRequest req, HttpServletResponse res)
190 throws IOException {
191- BlobstoreService blobstoreService = BlobstoreServiceFactory.getBlobstoreService();
192-
193- try {
194- BlobKey blobKey = getBlobKey(req);
195- blobstoreService.serve(blobKey, res);
196- } catch (GameFileNotExists e) {
197- //TODO: Construct a page with the error
198- }
199+ String path = req.getPathInfo();
200+ if (path.length() > 0 && path.charAt(0) == '/')
201+ path = path.substring(1);
202+ String[] pathSegments = path.split("/", 3);
203+ // XXX Assumes a devteam/game, not a promoted game
204+ if (pathSegments.length != 3)
205+ {
206+ res.sendError(404, "Not found");
207+ return;
208+ }
209+ String devteamName = pathSegments[0];
210+ String gameName = pathSegments[1];
211+ String filePath = pathSegments[2];
212+
213+ GameFileData file;
214+ GameFileContents contents;
215+ try
216+ {
217+ GameData game = GameGetter.getGameByDevTeam(devteamName,gameName);
218+ Key versionKey = game.getActiveVersion();
219+ GameVersionData version =
220+ GameVersionGetter.getGameVersion(versionKey);
221+ file = GameFileGetter.getGameFile(game, version, filePath);
222+ contents = GameFileGetter.getGameFileContents(file);
223+ }
224+ catch (DevTeamNotExists e)
225+ {
226+ res.sendError(404, e.toString());
227+ return;
228+ }
229+ catch (GameNotExists e)
230+ {
231+ res.sendError(404, e.toString());
232+ return;
233+ }
234+ catch (GameVersionNotExists e)
235+ {
236+ res.sendError(404, e.toString());
237+ return;
238+ }
239+ catch (GameFileNotExists e)
240+ {
241+ res.sendError(404, e.toString());
242+ return;
243+ }
244+ catch (GameFileContentsNotExists e)
245+ {
246+ res.sendError(404, e.toString());
247+ return;
248+ }
249+
250+ // Serve the file on the HTTP response
251+ res.setContentType(file.getMimeType());
252+ byte[] bytes = contents.getContents();
253+ res.setContentLength(bytes.length);
254+ res.getOutputStream().write(bytes);
255 }
256
257+ /*
258 private BlobKey getBlobKey(HttpServletRequest req)
259 throws GameFileNotExists {
260
261@@ -57,7 +112,7 @@
262 /* We need to parse the request to get the gameName, gameVersion
263 * and path,
264 * URLs are in the form /game/gameName/gameVersion/path
265- */
266+ * /
267 String toParse = req.getRequestURI().split("/game/")[1]; // gets everything after /game/ in the URI
268 String[] splitted = toParse.split("/");
269 gameName = splitted[0];
270@@ -74,6 +129,7 @@
271 }
272
273 }
274+ */
275
276 /*
277 public void doPost(HttpServletRequest req, HttpServletResponse res)
278
279=== added file 'src/au/edu/unimelb/csse/mugle/server/UploadService.java'
280--- src/au/edu/unimelb/csse/mugle/server/UploadService.java 1970-01-01 00:00:00 +0000
281+++ src/au/edu/unimelb/csse/mugle/server/UploadService.java 2011-05-22 18:34:23 +0000
282@@ -0,0 +1,197 @@
283+/* Melbourne University Game-based Learning Environment
284+ * Copyright (C) 2011 The University of Melbourne
285+ *
286+ * This program is free software: you can redistribute it and/or modify
287+ * it under the terms of the GNU General Public License as published by
288+ * the Free Software Foundation, either version 3 of the License, or
289+ * (at your option) any later version.
290+ *
291+ * This program is distributed in the hope that it will be useful,
292+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
293+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
294+ * GNU General Public License for more details.
295+ *
296+ * You should have received a copy of the GNU General Public License
297+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
298+ */
299+package au.edu.unimelb.csse.mugle.server;
300+
301+import java.io.InputStream;
302+import java.io.PrintWriter;
303+import java.io.IOException;
304+import javax.servlet.http.HttpServlet;
305+import javax.servlet.http.HttpServletRequest;
306+import javax.servlet.http.HttpServletResponse;
307+import javax.jdo.PersistenceManager;
308+
309+import java.util.zip.ZipInputStream;
310+import java.util.zip.ZipEntry;
311+import java.util.zip.ZipException;
312+
313+import org.apache.commons.fileupload.FileItemIterator;
314+import org.apache.commons.fileupload.FileItemStream;
315+import org.apache.commons.fileupload.servlet.ServletFileUpload;
316+import org.apache.commons.fileupload.FileUploadException;
317+
318+import java.util.Vector;
319+
320+import au.edu.unimelb.csse.mugle.server.model.GameFileData;
321+import au.edu.unimelb.csse.mugle.server.model.GameFileContents;
322+import au.edu.unimelb.csse.mugle.server.model.GameFileGetter;
323+import au.edu.unimelb.csse.mugle.server.model.GameVersionData;
324+import au.edu.unimelb.csse.mugle.server.PMF;
325+import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameFileNotExists;
326+import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameFileContentsNotExists;
327+
328+public class UploadService extends HttpServlet
329+{
330+ public void doPost(HttpServletRequest request,
331+ HttpServletResponse response) throws IOException
332+ {
333+ long gameversionID = Long.parseLong(
334+ request.getParameter("gameversion"));
335+ GameVersionData gameversion;
336+ boolean got_zip = false;
337+ int numfiles = 0;
338+ Vector<String> filenames = new Vector<String>();
339+
340+ // Read the POST data fields
341+ ServletFileUpload upload = new ServletFileUpload();
342+ PersistenceManager pm = PMF.getManager();
343+ try
344+ {
345+ // Get the GameVersion object
346+ gameversion =
347+ pm.getObjectById(GameVersionData.class, gameversionID);
348+ }
349+ finally
350+ {
351+ pm.close();
352+ }
353+ try
354+ {
355+ // For each field
356+ FileItemIterator iter = upload.getItemIterator(request);
357+ while (iter.hasNext())
358+ {
359+ FileItemStream item = iter.next();
360+ // Only care about the game_upload field
361+ if (!item.getFieldName().equals("game_upload"))
362+ continue;
363+ got_zip = true;
364+
365+ // Assume it is a ZIP file. Read each file contained inside.
366+ InputStream stream = item.openStream();
367+ ZipInputStream zstream = new ZipInputStream(stream);
368+ ZipEntry entry;
369+ while ((entry = zstream.getNextEntry()) != null)
370+ {
371+ if (entry.isDirectory())
372+ {
373+ // Ignore directories (since we are not building a
374+ // tree; just remembering the path to each file)
375+ continue;
376+ }
377+ String filename = entry.getName();
378+ // Remove the first path segment from the filename (since
379+ // ZIP files tend to have a top-level directory, and we
380+ // want to explode out of that).
381+ String[] filename_split = filename.split("/", 2);
382+ if (filename_split.length > 1)
383+ filename = filename_split[1];
384+ long size = entry.getSize();
385+ // Create a new GameFile object
386+ createGameFile(gameversion, filename, zstream, size);
387+ filenames.add(filename);
388+ numfiles++;
389+ }
390+ }
391+ }
392+ catch (FileUploadException e)
393+ {
394+ response.sendError(400, e.toString());
395+ return;
396+ }
397+ catch (ZipException e)
398+ {
399+ response.sendError(400, e.toString());
400+ return;
401+ }
402+ if (!got_zip)
403+ {
404+ response.sendError(400, "Missing POST field game_upload");
405+ return;
406+ }
407+
408+ // Set HTML content type, even though returning text
409+ // The result is shown in an alert, and if it's text, Firefox will add
410+ // "<pre>".
411+ response.setContentType("text/html");
412+ PrintWriter out = response.getWriter();
413+ out.println("Successfully uploaded a ZIP file with "
414+ + numfiles + " files:");
415+ for (String s : filenames)
416+ out.println("- " + s);
417+ }
418+
419+ /** Create a new GameFile and store it in the database, from a part of a
420+ * Zip file.
421+ * @param gameVersionKey The game version to upload this file to.
422+ * @param filePath Path to the file relative to this game version.
423+ * @param data Binary stream to read file contents from.
424+ * @param dataSize Number of bytes to read from data.
425+ */
426+ public static void createGameFile(GameVersionData gameVersion,
427+ String filePath, InputStream data, long dataSize)
428+ throws IOException
429+ {
430+ PersistenceManager pm = PMF.getManager();
431+ try
432+ {
433+ // Check if a file already exists at that path
434+ GameFileData file;
435+ GameFileContents contents;
436+ try
437+ {
438+ file = GameFileGetter.getGameFile(pm, gameVersion, filePath);
439+ }
440+ catch (GameFileNotExists e)
441+ {
442+ // First, store the contents as a blob in the database
443+ contents = new GameFileContents(data, dataSize);
444+ pm.makePersistent(contents);
445+ // Then, create a GameFile which points at that blob
446+ file = new GameFileData(gameVersion, filePath, contents);
447+ pm.makePersistent(file);
448+ return;
449+ }
450+ // Already exists; just update the file and contents
451+ // (Note that if we were uploading a new version, we wouldn't
452+ // overwrite the file or contents, but since we're updating an
453+ // existing version, we can replace the contents).
454+ // XXX Note that if contents are shared across versions this will
455+ // clobber ALL versions, so probably we should create a new
456+ // content object each time.
457+ try
458+ {
459+ contents = GameFileGetter.getGameFileContents(file);
460+ contents.setContents(data, dataSize);
461+ pm.makePersistent(contents);
462+ }
463+ catch (GameFileContentsNotExists e)
464+ {
465+ contents = new GameFileContents(data, dataSize);
466+ pm.makePersistent(contents);
467+ file.setContents(contents.getServerKey());
468+ }
469+ // Also set the MIME type based on the filename (so the result is
470+ // consistent with what would have happened if the file didn't
471+ // exist).
472+ file.setMimeTypeFromFilename();
473+ }
474+ finally
475+ {
476+ pm.close();
477+ }
478+ }
479+}
480
481=== added file 'src/au/edu/unimelb/csse/mugle/server/model/GameFileContents.java'
482--- src/au/edu/unimelb/csse/mugle/server/model/GameFileContents.java 1970-01-01 00:00:00 +0000
483+++ src/au/edu/unimelb/csse/mugle/server/model/GameFileContents.java 2011-05-22 18:34:23 +0000
484@@ -0,0 +1,104 @@
485+/* Melbourne University Game-based Learning Environment
486+ * Copyright (C) 2011 The University of Melbourne
487+ *
488+ * This program is free software: you can redistribute it and/or modify
489+ * it under the terms of the GNU General Public License as published by
490+ * the Free Software Foundation, either version 3 of the License, or
491+ * (at your option) any later version.
492+ *
493+ * This program is distributed in the hope that it will be useful,
494+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
495+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
496+ * GNU General Public License for more details.
497+ *
498+ * You should have received a copy of the GNU General Public License
499+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
500+ */
501+
502+package au.edu.unimelb.csse.mugle.server.model;
503+
504+import java.io.OutputStream;
505+import java.io.InputStream;
506+import java.io.IOException;
507+
508+import javax.jdo.PersistenceManager;
509+import javax.jdo.annotations.IdGeneratorStrategy;
510+import javax.jdo.annotations.PersistenceCapable;
511+import javax.jdo.annotations.Persistent;
512+import javax.jdo.annotations.PrimaryKey;
513+
514+import com.google.appengine.api.datastore.Key;
515+import com.google.appengine.api.datastore.Blob;
516+
517+/** The contents of a GameFile.
518+ * This simple class contains only the contents and no other meta-data. It
519+ * may be referenced by one or more GameFile objects.
520+ */
521+@PersistenceCapable
522+public class GameFileContents {
523+ // Fields
524+
525+ @PrimaryKey
526+ @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
527+ private Key id;
528+
529+ @Persistent
530+ private Blob data;
531+
532+ /** Create a new file contents blob, from a given input stream.
533+ @param size The number of bytes to read from stream.
534+ */
535+ public GameFileContents(InputStream stream, long size)
536+ throws IOException
537+ {
538+ this.setContents(stream, size);
539+ }
540+
541+ // Getters
542+
543+ public Key getServerKey() {
544+ return this.id;
545+ }
546+
547+ // Misc methods
548+
549+ /** Get the contents of the file as a byte array.
550+ */
551+ public byte[] getContents()
552+ {
553+ return this.data.getBytes();
554+ }
555+
556+ /** Write the contents of the file to the OutputStream object.
557+ * @param writer Stream object to receive the contents of the file.
558+ */
559+ public void getContents(OutputStream writer)
560+ throws IOException
561+ {
562+ byte[] bytes = this.data.getBytes();
563+ writer.write(bytes);
564+ }
565+
566+ /** Read the contents of an InputStream and store it in the database as
567+ * the contents of this file.
568+ * @param stream Stream object to read contents of file from.
569+ @param size The number of bytes to read from stream.
570+ */
571+ public void setContents(InputStream stream, long size)
572+ throws IOException
573+ {
574+ //TODO: check that the DevTeam's space limit hasn't been exceeded.
575+ byte[] bytes = new byte[(int)size];
576+ // Read in a loop
577+ // (stream.read might read fewer than the requested number of bytes)
578+ int offset = 0;
579+ int remaining = (int)size;
580+ while (remaining > 0)
581+ {
582+ int bytes_read = stream.read(bytes, offset, remaining);
583+ offset += bytes_read;
584+ remaining -= bytes_read;
585+ }
586+ this.data = new Blob(bytes);
587+ }
588+}
589
590=== modified file 'src/au/edu/unimelb/csse/mugle/server/model/GameFileData.java'
591--- src/au/edu/unimelb/csse/mugle/server/model/GameFileData.java 2011-05-22 11:06:46 +0000
592+++ src/au/edu/unimelb/csse/mugle/server/model/GameFileData.java 2011-05-22 18:34:23 +0000
593@@ -24,9 +24,10 @@
594 import javax.jdo.annotations.PersistenceCapable;
595 import javax.jdo.annotations.Persistent;
596 import javax.jdo.annotations.PrimaryKey;
597+import javax.activation.MimetypesFileTypeMap;
598
599-import com.google.appengine.api.blobstore.BlobKey;
600 import com.google.appengine.api.datastore.Key;
601+import com.google.appengine.api.datastore.Blob;
602
603 import au.edu.unimelb.csse.mugle.shared.model.*;
604 import au.edu.unimelb.csse.mugle.server.PMF;
605@@ -50,7 +51,7 @@
606 private String mimeType; //mime type of the file
607
608 @Persistent
609- private BlobKey blobKey; //key to the actual blob -- may be shared between GameFiles across versions
610+ private Key contents; //key to the GameFileContents
611
612 @Persistent
613 private Key version; //Game version this Gamefile belongs to
614@@ -72,23 +73,43 @@
615 public GameFileData() {
616 this.version = null;
617 this.setMimeType(null);
618- this.setBlobKey(null);
619 this.path = null;
620 }
621
622-/** TODO: Pending Blob implementation for files
623- // For creation of the file for the first time
624- public GameFile (String path, GameVersion version, InputStream contents) {
625- this.path = path;
626- this.versions.add(version.getPrimaryKey());
627- this.setContents(contents);
628- }
629-*/
630+ /** Create a new GameFileData object.
631+ * Guesses the file's MIME type based on the file extension.
632+ * @param version The game version the file belongs to.
633+ * @param path Path to the file relative to this gameversion.
634+ * @param contents Blob containing the file's contents.
635+ */
636+ public GameFileData(GameVersionData version, String path,
637+ GameFileContents contents)
638+ {
639+ this.version = version.getServerKey();
640+ this.path = path;
641+ this.setMimeTypeFromFilename();
642+ this.contents = contents.getServerKey();
643+ }
644+
645+ /** Create a new GameFileData object.
646+ * @param version The game version the file belongs to.
647+ * @param path Path to the file relative to this gameversion.
648+ * @param mimeType MIME type of the file.
649+ * @param contents Blob containing the file's contents.
650+ */
651+ public GameFileData(GameVersionData version, String path, String mimeType,
652+ GameFileContents contents)
653+ {
654+ this.version = version.getServerKey();
655+ this.path = path;
656+ this.mimeType = mimeType;
657+ this.contents = contents.getServerKey();
658+ }
659
660 /* When a file is moved, or renamed - The existing GameFile entry should be deleted,
661 * in which case, we may want to initialise with multiple Game versions.
662 */
663-/** TODO: Pending Blob implementation for files
664+/** TODO: Pending multiple versions of games
665 public GameFile (String path, Set<String> versions, InputStream contents) {
666 this.path = path;
667 this.versions = versions;
668@@ -122,18 +143,6 @@
669 return mimeType;
670 }
671
672- @GetterUserLevel(privateView=Role.GUEST, publicView=Role.GUEST, mappedTo="setBlobKey", overrideBy="getBlobKeyString")
673- public BlobKey getBlobKey() {
674- return this.blobKey;
675- }
676-
677- public String getBlobKeyString() {
678- if (this.blobKey == null) {
679- return null;
680- }
681- return this.blobKey.getKeyString();
682- }
683-
684 @Override
685 @GetterUserLevel(privateView=Role.GUEST, publicView=Role.GUEST, mappedTo="setCreatedUser", overrideBy="keyToCreated")
686 public Key getCreatedUser() {
687@@ -209,40 +218,30 @@
688 this.mimeType = mimeType;
689 }
690
691- // TODO: do we allow the clients to modify this value
692- //@SetterUserLevel(getter="", mappedBy="")
693- public void setBlobKey(BlobKey blobKey) {
694- this.blobKey = blobKey;
695- }
696-
697 // Misc methods
698
699- /** Write the contents of the file to the PrintWriter object.
700- * @param writer Stream object to receive the contents of the file.
701- */
702-/** TODO: Pending Blob implementation for files
703- public void getContents(PrintWriter writer) {
704- //TODO: retrieve file from database
705- }
706-*/
707-
708- /** Read the contents of an InputStream and store it in the database as
709- * the contents of this file.
710- * @param stream Stream object to read contents of file from.
711- */
712-/** TODO: Pending Blob implementation for files
713- public void setContents(InputStream stream) {
714- //TODO: add/create the file in the database
715- //TODO: check that the DevTeam's space limit hasn't been exceeded.
716- }
717-*/
718-
719-
720-/** TODO: Pending Blob implementation for files
721- public GameFileData(String path, GameVersion version, InputStream contents) {
722- super(path, version, contents);
723- }
724-*/
725+ /** Get the content blob of this file.
726+ * @return Key of the GameFileContents object.
727+ */
728+ public Key getContents() {
729+ return this.contents;
730+ }
731+
732+ /** Set the content blob of this file.
733+ * @param contents Key of the GameFileContents object.
734+ */
735+ public void setContents(Key contents) {
736+ this.contents = contents;
737+ }
738+
739+ /** Set the MIME type based on the file extension.
740+ */
741+ public void setMimeTypeFromFilename() {
742+ String f = this.path.toLowerCase();
743+ // This implicitly looks at the MIME mapping table in
744+ // src/META-INF/mime.types.
745+ this.mimeType = new MimetypesFileTypeMap().getContentType(this.path);
746+ }
747
748 @Override
749 public Key getServerKey() {
750
751=== modified file 'src/au/edu/unimelb/csse/mugle/server/model/GameFileGetter.java'
752--- src/au/edu/unimelb/csse/mugle/server/model/GameFileGetter.java 2011-05-22 11:06:46 +0000
753+++ src/au/edu/unimelb/csse/mugle/server/model/GameFileGetter.java 2011-05-22 18:34:23 +0000
754@@ -27,6 +27,7 @@
755
756 import au.edu.unimelb.csse.mugle.server.PMF;
757 import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameFileNotExists;
758+import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameFileContentsNotExists;
759 import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameNotExists;
760 import au.edu.unimelb.csse.mugle.shared.platform.exceptions.GameVersionNotExists;
761
762@@ -114,6 +115,27 @@
763
764 /**
765 * Gets the GameFile by its path, version and game, for editing object in datastore
766+ * @param gameName the name of the game
767+ * @param gameVersion the version of the game
768+ * @param path the path of the file
769+ * @return the GameFile - read only
770+ * @throws GameNotExists
771+ * @throws GameVersionNotExists
772+ * @throws GameFileNotExists
773+ */
774+ @SuppressWarnings("unchecked")
775+ public static GameFileData getGameFile(GameData game, GameVersionData gameVersion, String path)
776+ throws GameFileNotExists {
777+ PersistenceManager pm = PMF.getManager();
778+ try {
779+ return getGameFile(pm, game, gameVersion, path);
780+ } finally {
781+ pm.close();
782+ }
783+ }
784+
785+ /**
786+ * Gets the GameFile by its path, version and game, for editing object in datastore
787 * PersistenceManager must be handled by the caller
788 * @para pm The Persistence Manager
789 * @param gameName the name of the game
790@@ -126,14 +148,15 @@
791 */
792 @SuppressWarnings("unchecked")
793 public static GameFileData getGameFile(PersistenceManager pm, GameData game, GameVersionData gameVersion, String path)
794- throws GameFileNotExists, GameNotExists, GameVersionNotExists {
795+ throws GameFileNotExists {
796
797 Query q = pm.newQuery(GameFileData.class, "path == p && version == v");
798 q.declareParameters("String p, com.google.appengine.api.datastore.Key v");
799 List<GameFileData> results = (List<GameFileData>) q.execute(path, gameVersion.getServerKey());
800
801 if (results.isEmpty()) {
802- throw new GameFileNotExists(game.getUrlName(), String.valueOf(gameVersion), path);
803+ throw new GameFileNotExists(game.getUrlName(),
804+ gameVersion.getDisplayVersion(), path);
805 }
806 return results.get(0);
807 }
808@@ -160,13 +183,41 @@
809 List<GameFileData> results = (List<GameFileData>) q.execute(path, gvd.getServerKey());
810
811 if (results.isEmpty()) {
812- throw new GameFileNotExists(game.getUrlName(), String.valueOf(gameVersion), path);
813+ throw new GameFileNotExists(game.getUrlName(),
814+ Integer.toString(gameVersion), path);
815 }
816
817 return results.get(0);
818 }
819
820 /**
821+ * Gets the GameFile by its path and version, for editing object in
822+ * datastore.
823+ * PersistenceManager must be handled by the caller
824+ * @para pm The Persistence Manager
825+ * @param gameVersion the version of the game
826+ * @param path the path of the file
827+ * @return the GameFile - read only
828+ * @throws GameFileNotExists
829+ */
830+ @SuppressWarnings("unchecked")
831+ public static GameFileData getGameFile(PersistenceManager pm,
832+ GameVersionData gameVersion, String path)
833+ throws GameFileNotExists {
834+
835+ Query q = pm.newQuery(GameFileData.class, "path == p && version == v");
836+ q.declareParameters("String p, com.google.appengine.api.datastore.Key v");
837+ List<GameFileData> results = (List<GameFileData>) q.execute(path,
838+ gameVersion.getServerKey());
839+
840+ if (results.isEmpty()) {
841+ throw new GameFileNotExists(gameVersion.getDisplayVersion(), path);
842+ }
843+
844+ return results.get(0);
845+ }
846+
847+ /**
848 * Gets the GameFile associated with the given GameFileData primary key.
849 * @param pm The PersistenceManager
850 * @param key The server-side key (ModelDataClass.getServerKey())
851@@ -187,5 +238,39 @@
852 }
853
854 }
855-
856+
857+ /**
858+ * Gets the GameFileContents associated with the given GameFile.
859+ * @param file The file to retrieve contents for.
860+ * @return The GameFileContents object directly off the datastore.
861+ * @throws GameFileContentsNotExists
862+ */
863+ public static GameFileContents getGameFileContents(GameFileData file)
864+ throws GameFileContentsNotExists {
865+ PersistenceManager pm = PMF.getManager();
866+ try {
867+ return getGameFileContents(pm, file);
868+ } finally {
869+ pm.close();
870+ }
871+ }
872+
873+ /**
874+ * Gets the GameFileContents associated with the given GameFile.
875+ * @param pm The PersistenceManager
876+ * @param file The file to retrieve contents for.
877+ * @return The GameFileContents object directly off the datastore.
878+ * @throws GameFileContentsNotExists
879+ */
880+ public static GameFileContents getGameFileContents(PersistenceManager pm,
881+ GameFileData file) throws GameFileContentsNotExists {
882+ Key key = file.getContents();
883+ GameFileContents result = pm.getObjectById(GameFileContents.class,
884+ key);
885+ if (result == null)
886+ {
887+ throw new GameFileContentsNotExists(key.getId());
888+ }
889+ return result;
890+ }
891 }
892
893=== modified file 'src/au/edu/unimelb/csse/mugle/server/model/GameGetter.java'
894--- src/au/edu/unimelb/csse/mugle/server/model/GameGetter.java 2011-05-20 06:35:39 +0000
895+++ src/au/edu/unimelb/csse/mugle/server/model/GameGetter.java 2011-05-22 18:34:23 +0000
896@@ -185,15 +185,17 @@
897
898 /**
899 * Gets the Game by the name of the DevTeam it belongs to - READ ONLY
900- * @param name name of the DevTeam
901+ * @param devTeamName name of the DevTeam
902+ * @param gameName name of the Game
903 * @return the Game - read only
904 * @throws GameNotExists
905 * @throws DevTeamNotExists
906 */
907- public static GameData getGameByDevTeam(String name) throws GameNotExists, DevTeamNotExists {
908+ public static GameData getGameByDevTeam(String devTeamName,
909+ String gameName) throws GameNotExists, DevTeamNotExists {
910 PersistenceManager pm = PMF.getManager();
911 try {
912- return getGameByDevTeam(pm, name);
913+ return getGameByDevTeam(pm, devTeamName, gameName);
914 } finally {
915 pm.close();
916 }
917@@ -209,14 +211,17 @@
918 * @throws DevTeamNotExists
919 */
920 @SuppressWarnings("unchecked")
921- public static GameData getGameByDevTeam(PersistenceManager pm, String name) throws GameNotExists, DevTeamNotExists{
922- DevTeamData dtd = DevTeamGetter.getDevTeam(name);
923- Query q = pm.newQuery(GameData.class, "devTeam == d");
924- q.declareParameters("com.google.appengine.api.datastore.Key d");
925- List<GameData> results = (List<GameData>) q.execute(dtd.getServerKey());
926+ public static GameData getGameByDevTeam(PersistenceManager pm,
927+ String devTeamName, String gameName)
928+ throws GameNotExists, DevTeamNotExists{
929+ DevTeamData dtd = DevTeamGetter.getDevTeam(devTeamName);
930+ Query q = pm.newQuery(GameData.class, "devTeam == d && urlName == g");
931+ q.declareParameters("com.google.appengine.api.datastore.Key d, String g");
932+ List<GameData> results = (List<GameData>)
933+ q.execute(dtd.getServerKey(), gameName);
934
935 if(results.isEmpty()) {
936- throw new GameNotExists(name);
937+ throw new GameNotExists(devTeamName, gameName);
938 }
939
940 return results.get(0);
941
942=== modified file 'src/au/edu/unimelb/csse/mugle/server/model/GameVersionGetter.java'
943--- src/au/edu/unimelb/csse/mugle/server/model/GameVersionGetter.java 2011-05-22 07:40:42 +0000
944+++ src/au/edu/unimelb/csse/mugle/server/model/GameVersionGetter.java 2011-05-22 18:34:23 +0000
945@@ -155,6 +155,21 @@
946 }
947
948 /**
949+ * Gets the GameVersion by its Primary Key - READ ONLY
950+ * @param primaryKey
951+ * @return the GameVersion - read only
952+ * @throws GameVersionNotExists
953+ */
954+ public static GameVersionData getGameVersion(Key primaryKey) throws GameVersionNotExists {
955+ PersistenceManager pm = PMF.getManager();
956+ try {
957+ return getGameVersion(pm, primaryKey);
958+ } finally {
959+ pm.close();
960+ }
961+ }
962+
963+ /**
964 * Gets the GameVersion associated with the given GameVersionData primary key.
965 * @param pm The PersistenceManager
966 * @param key The server-side key (ModelDataClass.getServerKey())
967@@ -164,16 +179,11 @@
968 public static GameVersionData getGameVersion(PersistenceManager pm, Key key)
969 throws GameVersionNotExists {
970
971- try {
972- GameVersionData result = pm.getObjectById(GameVersionData.class, key);
973- if (result == null) {
974- throw new GameVersionNotExists(key.getId());
975- }
976- return result;
977- } finally {
978- pm.close();
979+ GameVersionData result = pm.getObjectById(GameVersionData.class, key);
980+ if (result == null) {
981+ throw new GameVersionNotExists(key.getId());
982 }
983-
984+ return result;
985 }
986
987 }
988
989=== added file 'src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileContentsNotExists.java'
990--- src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileContentsNotExists.java 1970-01-01 00:00:00 +0000
991+++ src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileContentsNotExists.java 2011-05-22 18:34:23 +0000
992@@ -0,0 +1,15 @@
993+package au.edu.unimelb.csse.mugle.shared.platform.exceptions;
994+
995+public class GameFileContentsNotExists extends MugleException {
996+
997+ private static final long serialVersionUID = 728229816363066583L;
998+
999+ public GameFileContentsNotExists() {
1000+ super();
1001+ }
1002+
1003+ public GameFileContentsNotExists(Long primaryKey) {
1004+ super("The file contents with primary key " + primaryKey.toString() +
1005+ " does not exist.");
1006+ }
1007+}
1008
1009=== modified file 'src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileNotExists.java'
1010--- src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileNotExists.java 2011-05-20 06:35:39 +0000
1011+++ src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameFileNotExists.java 2011-05-22 18:34:23 +0000
1012@@ -13,7 +13,12 @@
1013 }
1014
1015 public GameFileNotExists(String gameName, String gameVersion, String path) {
1016- super("The file " + path + " for the version " + gameVersion +
1017- " of " + gameName + " does not Exist.");
1018+ super("The file " + path + " for version " + gameVersion +
1019+ " of " + gameName + " does not exist.");
1020+ }
1021+
1022+ public GameFileNotExists(String gameVersion, String path) {
1023+ super("The file " + path + " for version " + gameVersion +
1024+ " of <unknown game> does not exist.");
1025 }
1026 }
1027
1028=== modified file 'src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameNotExists.java'
1029--- src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameNotExists.java 2011-05-22 07:40:42 +0000
1030+++ src/au/edu/unimelb/csse/mugle/shared/platform/exceptions/GameNotExists.java 2011-05-22 18:34:23 +0000
1031@@ -38,6 +38,11 @@
1032 super("Game '" + gameName + "' does not exist.");
1033 }
1034
1035+ public GameNotExists(String teamName, String gameName) {
1036+ super("Game '" + gameName + "' by team '" + teamName
1037+ + "' does not exist.");
1038+ }
1039+
1040 public GameNotExists(Long id) {
1041 super("Game with ID '" + id + "' does not exist.");
1042 }
1043
1044=== added file 'war/WEB-INF/lib/commons-fileupload-1.2.2.jar'
1045Binary 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 18:34:23 +0000 differ
1046=== modified file 'war/WEB-INF/web.xml'
1047--- war/WEB-INF/web.xml 2011-05-08 06:05:33 +0000
1048+++ war/WEB-INF/web.xml 2011-05-22 18:34:23 +0000
1049@@ -132,6 +132,28 @@
1050 <servlet-name>UserGameProfileService</servlet-name>
1051 <url-pattern>/mugle/data-ugp</url-pattern>
1052 </servlet-mapping>
1053+
1054+ <!-- Game upload and hosting services -->
1055+
1056+ <servlet>
1057+ <servlet-name>UploadService</servlet-name>
1058+ <servlet-class>au.edu.unimelb.csse.mugle.server.UploadService</servlet-class>
1059+ </servlet>
1060+
1061+ <servlet-mapping>
1062+ <servlet-name>UploadService</servlet-name>
1063+ <url-pattern>/mugle/upload</url-pattern>
1064+ </servlet-mapping>
1065+
1066+ <servlet>
1067+ <servlet-name>GameFileServer</servlet-name>
1068+ <servlet-class>au.edu.unimelb.csse.mugle.server.GameFileServer</servlet-class>
1069+ </servlet>
1070+
1071+ <servlet-mapping>
1072+ <servlet-name>GameFileServer</servlet-name>
1073+ <url-pattern>/+games/*</url-pattern>
1074+ </servlet-mapping>
1075
1076 <!-- API servlet mappings -->
1077

Subscribers

People subscribed via source and target branches