mstahv / e-xell

A demo app showing file centric web apps without persistency or authentication. Uploads, downloads and WebStorage with Java & Vaadin.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

File Centric Web Apps

This example illustrates a compact spreadsheet application. It allows you to create new files online, save them to your own workstation or your browser's local storage, and open them from these locations.

The app is using a Vaadin Spreadsheet component (commercial extension, all other parts like Spring Boot & Vaadin core are Apache 2 licensed OSS) to implement the actual "spreadsheet part" and there is an online version of this deployed on a tiny demo server. But the main point of the example is not to brag about the component that implements the spreadsheet editor with a one-liner. Use Google Sheets if you need a proper online spreadsheet application.

The beef of this example is how the data is handled. In a typical web app the database is a central part of your application, where pretty much all data is saved. In this web app, there is no database, it doesn't store anything on the filesystem nor to AWS S3 or similar. Users files exist on their own workstations, either on their normal file system or in their browser's local storage. The example demonstrate both the universal architectural approach where no data is saved on the server and the best practices to implement the actions with Java in JVM server using Vaadin.

The lack of server-side persistence simplifies the architecture significantly. The app serializes spreadsheet data to bytes via Apache POI, used by Vaadin Spreadsheet. In more typical cases you would generate JSON, use XML, standard Java serialization or e.g. Eclipse Serializer to store custom data structures. It is not only about not needing Hibernate, but, for example, there is no need to add authentication & authorization as the data don't need to be associated to any specific user(s). Not to mention that you might be able to workaround some nasty legal restrictions...

I have used this pattern both as a developer and consumer and I think it should get a bit more exposure among web developers. It suits very well for applications where sharing, collaboration or global access to your files is not needed. Also, the datasets that are handled in the app needs to be somewhat limited or at least in balance with your network connection. It is for example a good fit for various utility apps, whose data sets are not desired to exist in the cloud.

While this example runs in a JVM server, the same approach can be used in pure client-side apps as well. The File API, nowadays available in all modern browsers, technically allows you to read (and write) the file contents also using a JavaScript API. Naturally, with this approach you have a rather limited and varying execution environment (end user's browser's sandbox) compared to the superpowers of your server environment (any programming language, as much memory and CPUs as you can afford).

Some code highlights from the example app:

  • Downloading the spreadsheet file from a server to a workstation aka the "Save" action.
    private final DynamicFileDownloader saveItem = new DynamicFileDownloader(
    out -> spreadsheet.write(out)
    ).withTooltip("Downloads current file to disk...");
    Like most libraries the Spreadsheet component (and the underlying Apache POI library) writes the content to OutputStream. The DynamicFileDownloader component eats a lambda expression that is executed when the user clicks the button (technically an "A" tag). It is enough to simply pass the provided output stream to the Spreadsheet component. The bytes generated by POI are then written directly to the output stream, and finally written by the browser to a file on the end users file system. No extra buffers (memory or files) are needed.
  • Uploading the file from the workstation to teh server aka the "Open" action.
    private final UploadFileHandler openItem = new UploadFileHandler(
    (i, d) -> openExcellFile(i))
    .withAcceptedFileTypes(".xls", ".xlsx", "application/vnd.ms-excel")
    // make the file upload very compact by using
    // icon and disabling drag and drop area
    .withDragAndDrop(false)
    .withUploadButton(new VButton(VaadinIcon.UPLOAD.create())
    .withTooltip("Upload .xslx file...")
    );
    This is the opposite to the previous bullet. The UploadFileHandler component takes in a handler that consumes an InputStream. In addition, it gets some metadata about the file, like the name of the file in the user's filesystem, but those are not needed in this example. In the example the action instantiates a new Spreadsheet component that initializes its state from the contents of the file chosen by the user. Again no extra buffers are needed. By default, the component also supports drag and dropping files, but it would then be bit too large for the slim "toolbar". Thus, the code disables the drag and drop area and making it appear only as an icon, like the DynamicFileDownloader by default does.
  • Saving to the browsers WebStorage
    public static void write(String fileName, Consumer<OutputStream> writeTask) {
    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    try {
    GZIPOutputStream out = new GZIPOutputStream(bout);
    writeTask.accept(out);
    // WebStorage is string-string, base64 encode to String
    String data = Base64.getEncoder().encodeToString(bout.toByteArray());
    // Save to WebStorage and notify user
    WebStorage.setItem(fileName, data);
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }
    The actual UI code in the MainView is quite similar to the normal download discussed above. Instead of a file download we show a button that saves the content to the browsers WebStorage. The Java API around the WebStorage API has been shipped with Vaadin core since version 24.2.0, before that you needed to drop in a community supported add-on. In the example all the WebStorage access is separated to a generic helper class, that could in theory be re-used in other apps as well. The relevant part of it is shown above. As the WebStorage API in the browsers (and in the Java wrapper) is essentially a string-string map, the helper class makes Base64 encoding for the binary data. Before Base64 encoding, we also pass the byte stream through GZIPOutputStream to compress it. I didn't check if it really helps with this particular binary type (which might be compressed already), but added it blindly to the example as the default maximun size in "local storage" is "just" 5 megs (per domain). Depending on your use case, it may be a lot or not.
  • Reading the file input from the browsers WebStorage https://github.com/mstahv/e-xell/blob/main/src/main/java/org/example/views/WebStorageFilePicker.java#L162-L182 Reading from WebStorage is symmetric to the previous point. The helper first shows a dialog that allows user to choose from previously known "file names" (which are technically just keys into WebStorage). Once the key is chosen, its value is decoded and the gzip compression is removed, before it is passed back to the MainView claass and ultimately to the Vaadin Spreasheet component and the POI library.

As a summary, here are some pro's and con's of this kind of web apps:

  • No need for a database.
  • No need for JPA or any other persistence libraries.
  • No hassle in deployment to connect the application server to the database.
  • No database as a bottleneck when horizontally scaling your cluster for a huge deployment.
  • No need for backups, as long as your code is in GitHub
  • No need to implement authentication to your application, less security concerns. Your users files are controlled by your users.
  • Sharing files with other users is both harder and more complex. In this example files only live shortly during the session in the JVM memory, associated to an open browser window, making it pretty much impossible to share the file permanently with others via server. But on the other hand, files can be sent as an email attachments or on floppy disks and even backed up on tape 🤓 And it is harder for non-skilled users to accidentally share important documents to the whole world ("to anyone with a link"), that is proven to happen a lot with tools like Google Docs. Of course, if the device with the browser (or filesystem) gets compromised, or the application server's RAM, this kind of data can get hacked too!

Running the demo application

You'll need JDK, at least version 17. The project is a standard Maven project so you can run it from command line or via all commonly used Java IDEs. To run it from the command line, type mvn, then open http://localhost:8080 in your browser.

You can also import the project to your IDE of choice as you would with any Maven project. Read more on how to set up a development environment for Vaadin projects (Windows, Linux, macOS).

Deploying to Production

To create a production build, call mvn clean package -Pproduction. This will build a JAR file with all the dependencies and front-end resources, ready to be deployed. The file can be found in the target folder after the build completes.

Once the JAR file is built, you can run it using java -jar target/myapp-1.0-SNAPSHOT.jar (NOTE, replace myapp-1.0-SNAPSHOT.jar with the name of your jar).

Project structure

  • MainView.java in src/main/java is an example Vaadin view.
  • src/main/resources contains configuration files and static resources
  • The frontend directory in the root folder is where client-side dependencies and resource files should be placed.

Useful links

About

A demo app showing file centric web apps without persistency or authentication. Uploads, downloads and WebStorage with Java & Vaadin.

License:The Unlicense


Languages

Language:Java 100.0%