From 471933ca8c60a29392e485279ffec25e9806ff03 Mon Sep 17 00:00:00 2001 From: jendib Date: Sun, 28 Jul 2013 18:29:03 +0200 Subject: [PATCH] PDF handling, file upload progression --- docs-core/pom.xml | 4 +- .../docs/core/dao/lucene/ArticleDao.java | 11 --- .../java/com/sismics/util/mime/MimeType.java | 2 + .../com/sismics/util/mime/MimeTypeUtil.java | 2 + docs-parent/pom.xml | 8 +- .../util/filter/RequestContextFilter.java | 27 +++--- .../docs/rest/resource/FileResource.java | 42 ++++++---- docs-web/src/main/webapp/js/app.js | 10 +-- .../main/webapp/js/controller/DocumentEdit.js | 60 ++++++++----- .../src/main/webapp/js/controller/FileView.js | 84 ++++++++++++------- .../main/webapp/partial/document.edit.html | 8 +- .../main/webapp/partial/document.view.html | 4 +- .../src/main/webapp/partial/file.view.html | 12 ++- .../sismics/docs/rest/TestFileResource.java | 17 +++- 14 files changed, 186 insertions(+), 105 deletions(-) delete mode 100644 docs-core/src/main/java/com/sismics/docs/core/dao/lucene/ArticleDao.java diff --git a/docs-core/pom.xml b/docs-core/pom.xml index 3ef7d079..dcde9a75 100644 --- a/docs-core/pom.xml +++ b/docs-core/pom.xml @@ -113,8 +113,8 @@ - org.bitlet - weupnp + org.imgscalr + imgscalr-lib diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/lucene/ArticleDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/lucene/ArticleDao.java deleted file mode 100644 index cc418ebf..00000000 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/lucene/ArticleDao.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.sismics.docs.core.dao.lucene; - - -/** - * Lucene Article DAO. - * - * @author bgamard - */ -public class ArticleDao { - -} diff --git a/docs-core/src/main/java/com/sismics/util/mime/MimeType.java b/docs-core/src/main/java/com/sismics/util/mime/MimeType.java index bde9ac2e..9e1d6916 100644 --- a/docs-core/src/main/java/com/sismics/util/mime/MimeType.java +++ b/docs-core/src/main/java/com/sismics/util/mime/MimeType.java @@ -16,4 +16,6 @@ public class MimeType { public static final String IMAGE_GIF = "image/gif"; public static final String APPLICATION_ZIP = "application/zip"; + + public static final String APPLICATION_PDF = "application/pdf"; } diff --git a/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java b/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java index a8c1565b..1fd5f556 100644 --- a/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java +++ b/docs-core/src/main/java/com/sismics/util/mime/MimeTypeUtil.java @@ -37,6 +37,8 @@ public class MimeTypeUtil { return MimeType.IMAGE_PNG; } else if (headerBytes[0] == ((byte) 0x00) && headerBytes[1] == ((byte) 0x00) && headerBytes[2] == ((byte) 0x01) && headerBytes[3] == ((byte) 0x00)) { return MimeType.IMAGE_X_ICON; + } else if (headerBytes[0] == ((byte) 0x25) && headerBytes[1] == ((byte) 0x50) && headerBytes[2] == ((byte) 0x44) && headerBytes[3] == ((byte) 0x46)) { + return MimeType.APPLICATION_PDF; } return null; diff --git a/docs-parent/pom.xml b/docs-parent/pom.xml index fca3671a..92ac753f 100644 --- a/docs-parent/pom.xml +++ b/docs-parent/pom.xml @@ -34,7 +34,7 @@ r156 4.2.0 1.0.5 - 0.1.2 + 4.2 1.9.18-m 4.1.0.Final @@ -430,9 +430,9 @@ - org.bitlet - weupnp - ${org.bitlet.weupnp.version} + org.imgscalr + imgscalr-lib + ${org.imgscalr.imgscalr-lib.version} diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java index 3a695c05..076ff60f 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/RequestContextFilter.java @@ -105,21 +105,24 @@ public class RequestContextFilter implements Filter { } catch (Exception e) { ThreadLocalContext.cleanup(); - log.error("An exception occured, rolling back current transaction", e); + // IOException are thrown if the client closes the connection before completion + if (!(e instanceof IOException)) { + log.error("An exception occured, rolling back current transaction", e); - // If an unprocessed error comes up from the application layers (Jersey...), rollback the transaction (should not happen) - if (em.isOpen()) { - if (em.getTransaction() != null && em.getTransaction().isActive()) { - em.getTransaction().rollback(); - } - - try { - em.close(); - } catch (Exception ce) { - log.error("Error closing entity manager", ce); + // If an unprocessed error comes up from the application layers (Jersey...), rollback the transaction (should not happen) + if (em.isOpen()) { + if (em.getTransaction() != null && em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + + try { + em.close(); + } catch (Exception ce) { + log.error("Error closing entity manager", ce); + } } + throw new ServletException(e); } - throw new ServletException(e); } ThreadLocalContext.cleanup(); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 02366947..805cc57f 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -1,9 +1,7 @@ package com.sismics.docs.rest.resource; import java.io.BufferedInputStream; -import java.io.FilenameFilter; import java.io.InputStream; -import java.nio.file.Files; import java.nio.file.Paths; import java.text.MessageFormat; import java.util.ArrayList; @@ -33,6 +31,8 @@ import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ServerException; import com.sismics.rest.util.ValidationUtil; +import com.sismics.util.FileUtil; +import com.sismics.util.ImageUtil; import com.sismics.util.mime.MimeTypeUtil; import com.sun.jersey.multipart.FormDataBodyPart; import com.sun.jersey.multipart.FormDataParam; @@ -129,8 +129,8 @@ public class FileResource extends BaseResource { file.setMimeType(mimeType); String fileId = fileDao.create(file); - // Copy the incoming stream content into the storage directory - Files.copy(is, Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId)); + // Save the file + FileUtil.save(is, file); // Always return ok JSONObject response = new JSONObject(); @@ -222,22 +222,36 @@ public class FileResource extends BaseResource { @Path("{id: [a-z0-9\\-]+}/data") @Produces(MediaType.APPLICATION_OCTET_STREAM) public Response data( - @PathParam("id") final String id) throws JSONException { + @PathParam("id") final String id, + @QueryParam("thumbnail") boolean thumbnail) throws JSONException { if (!authenticate()) { throw new ForbiddenClientException(); } // Get the file - java.io.File storageDirectory = DirectoryUtil.getStorageDirectory(); - java.io.File[] matchingFiles = storageDirectory.listFiles(new FilenameFilter() { - @Override - public boolean accept(java.io.File dir, String name) { - return name.startsWith(id); - } - }); - final java.io.File storageFile = matchingFiles[0]; + FileDao fileDao = new FileDao(); + File file = null; + try { + file = fileDao.getFile(id); + } catch (NoResultException e) { + throw new ClientException("FileNotFound", MessageFormat.format("File not found: {0}", id)); + } - return Response.ok(storageFile) + + // Get the stored file + java.io.File storedfile = null; + if (thumbnail) { + if (ImageUtil.isImage(file.getMimeType())) { + storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), id + "_thumb").toFile(); + } else { + storedfile = new java.io.File(getClass().getResource("/image/file.png").getFile()); + } + } else { + storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), id).toFile(); + } + + return Response.ok(storedfile) + .header("Content-Type", file.getMimeType()) .build(); } } diff --git a/docs-web/src/main/webapp/js/app.js b/docs-web/src/main/webapp/js/app.js index bcb775d1..737c2cf4 100644 --- a/docs-web/src/main/webapp/js/app.js +++ b/docs-web/src/main/webapp/js/app.js @@ -67,14 +67,10 @@ var App = angular.module('docs', ['ui.state', 'ui.bootstrap', 'ui.keypress', 're }) .state('document.view.file', { url: '/file/:fileId', - onEnter: function($stateParams, $state, $dialog) { - $dialog.dialog({ - keyboard: true, - templateUrl: 'partial/file.view.html', + views: { + 'file': { controller: 'FileView' - }).open().then(function(result) { - $state.transitionTo('document.view', { id: $stateParams.id }); - }); + } } }) .state('login', { diff --git a/docs-web/src/main/webapp/js/controller/DocumentEdit.js b/docs-web/src/main/webapp/js/controller/DocumentEdit.js index 873055d7..67df4045 100644 --- a/docs-web/src/main/webapp/js/controller/DocumentEdit.js +++ b/docs-web/src/main/webapp/js/controller/DocumentEdit.js @@ -3,7 +3,7 @@ /** * Document edition controller. */ -App.controller('DocumentEdit', function($scope, $http, $state, $stateParams, Restangular) { +App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams, Restangular) { /** * Returns true if in edit mode (false in add mode). */ @@ -28,37 +28,55 @@ App.controller('DocumentEdit', function($scope, $http, $state, $stateParams, Res if ($scope.isEdit()) { promise = Restangular - .one('document', $stateParams.id) - .post('', $scope.document); - promise.then(function(data) { - $scope.loadDocuments(); - $state.transitionTo('document.view', { id: $stateParams.id }); - }) + .one('document', $stateParams.id) + .post('', $scope.document); } else { promise = Restangular - .one('document') - .put($scope.document); - promise.then(function(data) { - $scope.document = {}; - $scope.loadDocuments(); - }); + .one('document') + .put($scope.document); } // Upload files after edition - // TODO Handle file upload progression and errors promise.then(function(data) { - _.each($scope.files, function(file) { + var promises = []; + $scope.fileProgress = 0; + + _.each($scope.newFiles, function(file) { + // Build the payload var formData = new FormData(); formData.append('id', data.id); formData.append('file', file); - $.ajax({ - url: 'api/file', - type: 'PUT', - data: formData, - processData: false, - contentType: false + + // Send the file + var promiseFile = $http.put('api/file', + formData, { + headers: { 'Content-Type': false }, + transformRequest: function(data) { return data; } }); + + // TODO Handle progression when $q.notify will be released + + promiseFile.then(function() { + $scope.fileProgress += 100 / _.size($scope.newFiles); + }); + + // Store the promise for later + promises.push(promiseFile); }); + + // When all files upload are over, move on + var promiseAll = $q.all(promises); + if ($scope.isEdit()) { + promiseAll.then(function(data) { + $scope.loadDocuments(); + $state.transitionTo('document.view', { id: $stateParams.id }); + }); + } else { + promiseAll.then(function(data) { + $scope.document = {}; + $scope.loadDocuments(); + }); + } }); }; diff --git a/docs-web/src/main/webapp/js/controller/FileView.js b/docs-web/src/main/webapp/js/controller/FileView.js index cd2e4d20..e61e72fc 100644 --- a/docs-web/src/main/webapp/js/controller/FileView.js +++ b/docs-web/src/main/webapp/js/controller/FileView.js @@ -3,34 +3,62 @@ /** * File view controller. */ -App.controller('FileView', function($rootScope, $state, $scope, $stateParams) { - $scope.id = $stateParams.fileId; - - /** - * Navigate to the next file. - */ - $scope.nextFile = function() { - _.each($rootScope.files, function(value, key, list) { - if (value.id == $scope.id) { - var next = $rootScope.files[key + 1]; - if (next) { - $state.transitionTo('document.view.file', { id: $stateParams.id, fileId: next.id }); +App.controller('FileView', function($dialog, $state, $stateParams) { + var dialog = $dialog.dialog({ + keyboard: true, + templateUrl: 'partial/file.view.html', + controller: function($rootScope, $scope, $state, $stateParams) { + $scope.id = $stateParams.fileId; + + // Search current file + _.each($rootScope.files, function(value, key, list) { + if (value.id == $scope.id) { + $scope.file = value; } - } - }); - }; + }); + + /** + * Navigate to the next file. + */ + $scope.nextFile = function() { + _.each($rootScope.files, function(value, key, list) { + if (value.id == $scope.id) { + var next = $rootScope.files[key + 1]; + if (next) { + dialog.close({}); + $state.transitionTo('document.view.file', { id: $stateParams.id, fileId: next.id }); + } + } + }); + }; + + /** + * Navigate to the previous file. + */ + $scope.previousFile = function() { + _.each($rootScope.files, function(value, key, list) { + if (value.id == $scope.id) { + var previous = $rootScope.files[key - 1]; + if (previous) { + dialog.close({}); + $state.transitionTo('document.view.file', { id: $stateParams.id, fileId: previous.id }); + } + } + }); + }; + + /** + * Open the file in a new window. + */ + $scope.openFile = function() { + window.open('api/file/' + $scope.id + '/data'); + }; + } + }); - /** - * Navigate to the previous file. - */ - $scope.previousFile = function() { - _.each($rootScope.files, function(value, key, list) { - if (value.id == $scope.id) { - var previous = $rootScope.files[key - 1]; - if (previous) { - $state.transitionTo('document.view.file', { id: $stateParams.id, fileId: previous.id }); - } - } - }); - }; + dialog.open().then(function(result) { + if (result == null) { + $state.transitionTo('document.view', { id: $stateParams.id }); + } + }); }); \ No newline at end of file diff --git a/docs-web/src/main/webapp/partial/document.edit.html b/docs-web/src/main/webapp/partial/document.edit.html index aa618cac..04684a35 100644 --- a/docs-web/src/main/webapp/partial/document.edit.html +++ b/docs-web/src/main/webapp/partial/document.edit.html @@ -14,11 +14,15 @@
- +
- \ No newline at end of file + + +
+
+
\ No newline at end of file diff --git a/docs-web/src/main/webapp/partial/document.view.html b/docs-web/src/main/webapp/partial/document.view.html index 4713eee8..4e0ae103 100644 --- a/docs-web/src/main/webapp/partial/document.view.html +++ b/docs-web/src/main/webapp/partial/document.view.html @@ -14,7 +14,7 @@
  • - +

    @@ -24,3 +24,5 @@

  • + +
    \ No newline at end of file diff --git a/docs-web/src/main/webapp/partial/file.view.html b/docs-web/src/main/webapp/partial/file.view.html index 14587db5..a4338c57 100644 --- a/docs-web/src/main/webapp/partial/file.view.html +++ b/docs-web/src/main/webapp/partial/file.view.html @@ -3,5 +3,15 @@ + +
    + +
    + + + + + +
    +
    - \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index fec94c5b..604484c4 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -76,16 +76,29 @@ public class TestFileResource extends BaseJerseyTest { // Get the file data fileResource = resource().path("/file/" + file1Id + "/data"); fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken)); - response = fileResource.get(ClientResponse.class); + MultivaluedMapImpl getParams = new MultivaluedMapImpl(); + getParams.putSingle("thumbnail", false); + response = fileResource.queryParams(getParams).get(ClientResponse.class); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); InputStream is = response.getEntityInputStream(); byte[] fileBytes = ByteStreams.toByteArray(is); Assert.assertEquals(163510, fileBytes.length); + // Get the thumbnail data + fileResource = resource().path("/file/" + file1Id + "/data"); + fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken)); + getParams = new MultivaluedMapImpl(); + getParams.putSingle("thumbnail", true); + response = fileResource.queryParams(getParams).get(ClientResponse.class); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + is = response.getEntityInputStream(); + fileBytes = ByteStreams.toByteArray(is); + Assert.assertEquals(41935, fileBytes.length); + // Get all files from a document fileResource = resource().path("/file/list"); fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken)); - MultivaluedMapImpl getParams = new MultivaluedMapImpl(); + getParams = new MultivaluedMapImpl(); getParams.putSingle("id", document1Id); response = fileResource.queryParams(getParams).get(ClientResponse.class); json = response.getEntity(JSONObject.class);