Java Web Application Blog

I let AI rebuild a dead Java desktop app for the web. Here's where it broke.

Written by Enver Haase-Beer | Jul 16, 2026 8:39:14 AM

I came to RSS about twenty years late. When I finally went looking for a good reader, one beloved old Eclipse desktop app kept turning up on every "best of" list—RSSOwl—and it wouldn't even launch on my Mac. I'm a developer, so instead of shrugging I got nosy: how hard would it actually be to rebuild its main screen for the browser? Here's what I found, what came across in an afternoon, what fought me, and what turned out to be impossible.

I read a fair amount of developer stuff (the Vaadin blog, the Spring blog, a couple of Java ones, Hacker News), and half of it only really comes in email now. My inbox had become a graveyard of unread content. RSS promised to put it all in one place, newest first, without giving my email address to anyone – and let me actually get through it.

So I looked around for something to read it in. One name kept coming up, on nearly every "best RSS reader" list (it's still near the top of SourceForge's Java RSS readers for Mac) and in half the old forum threads: RSSOwl, a gorgeous Eclipse desktop reader with a classic three-pane layout — a tree of feeds on the left, a sortable table of headlines top-right, an article reader below. The lists tended to add the same asterisk — needs Java, looks dated — which I ignored. I downloaded it. Nothing happened. It just won't run on a 2026 Mac.

The dead binary (and the plot twist)

The official Mac build of RSSOwl won't start at all on a modern Mac. It’s an old 32-bit binary linked against Carbon, a UI layer Apple killed years ago Rosetta can't help it — it only translates 64-bit Intel code. The flagship download of the app simply cannot run on a 2026 machine. That's what "stranded on a dying desktop toolkit" actually feels like.

Then along came the maintained community fork of RSSOwlnix, distributed on GitHub, building a 64-bit Intel binary. That one does run on a current Mac, but under Rosetta, which itself is on the way out.


So I built it myself from source, and in just over two minutes I got a native Apple Silicon app that runs with no Rosetta at all because modern SWT ships a current
aarch64 build. It took changing one line in the build config (a Tycho target environment from x86_64 to aarch64) to get it.

"You cannot run SWT on Apple Silicon" is simply false. The problem is not the toolkit, but the distribution.

That distinction has stuck with me and is a good reason to move something like that to the web. It's not that the desktop toolkit can't keep up — it can. The problem is that the only build a normal person can actually get is a crumbling 32-bit relic (or an Intel one riding Rosetta, itself on the way out), and getting a modern native build requires source access, a whole Maven/Tycho toolchain, the right JDK, and knowing exactly which knob to turn. No one who downloads a feed reader is going to do that. But you just opened a web app.

So, as a web developer, I couldn't help but ask myself:

Could I take that exact screen — a feeds tree, a sortable headlines table with a right-click menu, and an article reader — and have an AI rebuild it in the browser? Maybe even make it multi-user, so friends could have their own feed lists? And where could it fall apart?

So I decided to find out, using Claude Code to do the typing and keeping a tally of what worked and what didn't. I was targeting Vaadin 25 (Java, server-side UI — not a JS rewrite, that was the point). Short version: the screen is moving. Much of it moves surprisingly fast. But “where does it fall apart” has some real answers. And the most interesting answer has nothing to do with widgets.

What came across clean and fast

The headlines table is the core of RSSOwl. It looks like a plain table, but it's secretly a tree (so it can group rows), with sortable columns, bold unread rows, custom row colors, clickable in-cell icons, and a right-click menu. That's the part I didn't think would be easy to move.

Most of it came fast:

  • Selecting a headline updating the reader "just worked" — via Signals.
    The AI was trying to peddle me something stale here at first:
    it wired the master→detail link with old-style change listeners. Vaadin 25 has a better solution — Signals. The headline that is selected lives in a ValueSignal<NewsItem>, and the reader binds to it reactively (Signal.effect(...)); there's no manual plumbing of "when the selection changes, go update the reader." Better than the desktop original, though, but I only found it because I checked, which is a theme below.
  • Sorting was almost free. You get the built-in click-to-sort headers, and the little sort arrow. Just give each column a Comparator.
  • The right-click menu was rendered faithfully. A GridContextMenu rebuilt on each open reproduced "Mark read" flipping to "Mark unread," and correctly shows nothing when you right-click a group header.

And then the thing that really surprised me: how much code just disappeared. Counting real lines (comments excluded):

RSSOwl's NewsComparator has gone from 306 lines to about 10 lines of Grid column comparators. Its 467-line NewsTableLabelProvider — bold unread, highlighted "sticky" rows, label colours — became some 10 lines of CSS.

All that imperative desktop ceremony — comparators, content providers, label providers, hand-drawn row painting — collapses into a bit of declarative Grid config and some CSS. (To be fair, some of the shrink is because my version does a little less than the original in the edge cases. But even accounting for that, the difference is enormous.)

Where the AI let me down

This is the part most articles skip, and it's the part I'd most like to read.
Common thread: none of the issues below came from anything the AI knew — they came from the compiler, from running the app (sometimes the AI itself caught them, by driving the app with Playwright), or from me comparing against how the original behaved.

  • It confidently wrote APIs that don't exist, because it had to. Here's how it works, and it's the single most important thing to understand before you allow an AI to work on a modern framework: the model has a cutoff date for its training data, and Vaadin 25 was released after that date. So this API has never been seen by the model. Ask it anyway and it doesn't say "I don't know" — it pattern-matches on the older Vaadin it learned on and gives you confident, plausible, wrong code (deprecated constructors, moved methods, the pre-Signals listener style). It looks fine, but it will not compile. 

    Without live docs, the AI answers your Vaadin 25 questions from pre-25 (Vaadin 24-era) training data — fluently, and incorrectly. This is why you need the MCP server.

    The fix is that Vaadin ships an MCP server that feeds its current docs directly to the AI. Consider the model's built-in framework knowledge as to be stale cache and make querying the MCP server for the real 25.x API to be a required step, not an afterthought. That one habit is what brought Signals, above, the current security config and the modern Upload/Download handlers into the light of day instead of fiction.
    Even with the right API, there are still sharp edges the AI face-planted on: a Grid row double-click event gives you the item directly, but the context-menu event gives you an Optional<T> — the same "get the clicked row," written two different ways.
  • A styling trick straight from the docs silently did nothing. The docs say to use the --vaadin-grid-cell-background custom property to color a row’s background. I did. It was applied to the cell, and the background stubbornly remained white. With the current Aura theme, the cell background is painted separately, so the property is ignored. The only thing that worked was background-color: … !important for the cell's ::part(...). I only found this out by inspecting the live shadow DOM. One-line fix - and a reminder that even doc-sanctioned advice should be verified against the running app. This would have taken probably an hour to figure out - luckily, the AI caught it with Playwright.
  • You can't guess, a Signal's got you. Reading the selection from a background timer with signal.get() throws — get() sets up reactive dependency tracking and is illegal outside an effect. You'll have to use signal.peek() from a plain callback. Obvious in hindsight, invisible until you ran the app. And again, only by opening the app did it surface.
  • A sorting subtlety it got quietly wrong. RSSOwl always sorts undated items to the bottom, either direction. Vaadin gives a column one comparator and reverses it for a descending sort, so my nullsLast flipped to nullsFirst and empty-date rows leapt to the top of the default newest-first view. Wrong in a way you'd only notice if you knew the original.

But the biggest lesson was bigger than any single bug. The AI made things on its own that looked right and weren’t. It created a set of feed categories that were different from the real ones in RSSOwl. It quietly shipped fewer feeds than the original and never mentioned it. It made up an arbitrary limit for how many articles to keep — until I pointed it at RSSOwl's actual source, where the real default (200 per feed) was sitting in plain sight. Every one of those looked completely plausible.

The AI got me a faithful skeleton, in hours. It needed someone who could replicate the behaviour of the original to turn it into a faithful app — someone who, this time, was me.

That’s the one thing I would say to anyone trying this. The tooling reproduces structure amazingly fast and under-delivers completeness just as reliably. It nails the broad 80% in an afternoon; the last 20% — the fidelity that makes it really the same app and not a convincing lookalike — needs someone who remembers what the original actually did (or who would figure it out). Not as a nice-to-have. As the factor that decides whether the result is correct.

What I could just not do

Some things didn't so much fight me as slam a door. Briefly:

  • Eclipse's pluggable menus have no web equivalent. Other plugins can add items to a menu via Eclipse extension registry (in RCP). That's an architecture, not a widget. And there is just nothing on the web to map it onto. That’s a redesign, not a port, if an app uses that kind of extensibility
  • A real embedded browser is blocked by the web itself. RSSOwl has a real browser embedded with the live page for the reader Drop a news site into an <iframe> today and it renders blank - sites forbid it with X-Frame-Options / CSP headers. I render the feed's own article HTML inline instead, first cleaned through a jsoup allow-list, because Vaadin's Html component doesn't sanitize, a documented footgun. Not the same thing but close in spirit.
  • Some features are just gone. RSSOwl syncs with Google Reader — a service that Google shut down in 2013. If the other half is gone, you can't move a feature.
  • The awkward rendering case is custom row colour plus the selection highlight. An arbitrary colour per-row and the selected-row state don’t compose cleanly through CSS parts, so I settled for a compromise rather than a perfect match.

None of these are really the web framework's fault. They're the real edges of dragging a 20-year-old desktop app onto today's platform. Knowing where those edges are before you start is worth more than any feature list.

A bonus I didn't count on

I just wanted to prove the main screen could move. But every time I checked "could this part move too?" — logins, full-text search, filters, labels, keyboard shortcuts — it turned into a working feature, and I ended up with the multi-user feed reader I'd half-jokingly asked for, one I'd actually use. It says a lot that the real thing sort of came out of just poking at it.. (With the caveats above firmly attached: I still had to catch every completeness gap myself, and one polished screen is not the same as porting a whole application shell.)

So what did it take?

  • Just that dense headlines table alone, with sorting and custom rendering and clickable cells, and the dynamic menu, and multi-select, and the timing behaviours, that was a solid couple of weeks of focused evenings with the AI helping. It's only about 2,000 lines of code but the behavior is dense. If you’re estimating your own, remember that ratio: small widget, heavy behaviour.
  • The entire three-pane screen, done faithfully, was realistic in that same couple-of-weeks window — with the AI, the Vaadin MCP server answering API questions, and me watching for the things it got subtly wrong.
  • A full RCP application would be a different beast — and most of what's left isn't widgets at all. The workbench, the perspectives, the command framework, the plugin system: that's the real cost, and the part with no clean equivalent. Redrawing screens is the fast, cheap part.

If I distilled the method to a few rules:

  • don't trust the AI's memory of the framework — point it at the vendor's live docs (for Vaadin, the MCP server) first.

  • plan the gnarly parts before writing code, so the "there's no equivalent for this" cases get surfaced early;

  • verify by running the app, not by reading the diff (the CSS bug, the Signals throw, and the sort flip were all invisible on paper);

  • and stay in the loop yourself on anything where fidelity to the original matters — the AI won't.

Want to poke at it?

All of it is on GitHub:

git clone https://github.com/vaadin/modernization-experiments.git cd modernization-experiments/swt-rcp-to-vaadin/poc/headlines ./mvnw spring-boot:run        # needs a JDK 21+; the first run pulls the frontend toolchain

One caveat, and it's the price of making this multi-user: the app gates every page behind an OIDC login, so it doesn't just open — it needs a Keycloak to authenticate against. Out of the box application.properties points at my Keycloak, which won't do you any good, so you'll have to stand up your own. The repo has a one-shot keycloak/setup-keycloak.sh that provisions the realm, a confidential client, and two test users (alice/alice, bob/bob) against a local Keycloak; then point spring.security.oauth2.client.provider.keycloak.issuer-uri at your instance and pass the client secret via the KEYCLOAK_CLIENT_SECRET env var (see run.sh). It's a few minutes of setup, not the one-command kind — the multi-user angle was a bonus I chased, and this is what it costs.

Once Keycloak is up and the app is running, open http://localhost:8080, and log in as alice.

If you've got your own stranded desktop app, the prompts I used are in the repo too — the part that's genuinely reusable: build your app from source first to prove it still runs, map its tables and trees to Grid/TreeGrid the same way, query the framework's live docs (the MCP server) instead of trusting the AI's memory, and verify everything by actually running it. These aren’t a magic recipe, but use them as inspiration for your own attempt.

Hit a wall with a weird framework quirk? Drop it in the Vaadin Forum - Java developer discussions. The community and our team are always around to help untangle edge cases.

If you have a huge legacy codebase and want a real reality check, maybe an architectural gut check or a full migration strategy, just get in touch with us directly through Contact Page. We’ll help you figure out the best way to finally get your app off the desktop and onto the modern web.

I went in expecting to prove it really couldn’t be done. I came out with the old screen running in a browser tab. No install, no Rosetta, no 32-bit ghosts. A much clearer sense of exactly where the hard parts are. A feed reader I'll actually use, appropriately enough. A late adopter, indeed.