URL Serialization

Last modified by Vincent Massol on 2026/11/02 20:53

Description

Modules: xwiki-platform-resource-*, xwiki-platform-url-*, xwiki-platform-oldcore (URL factories).

Goal

Generate every XWiki URL (pages, attachments, skins, resources, webjars, temporary files, users, REST, exports…) through the Resource and URL modules, with the same per-format rules that already parse incoming URLs. XWikiURLFactory and its subclasses then become thin adapters over it, and can later be deprecated.

Parsing already works this way: ResourceTypeResolver<ExtendedURL> → ResourceReferenceResolver<ExtendedURL> for the configured URL format. Serialization only has a skeleton: there is no serializer for pages and attachments, and producing an absolute URL isn't possible (URLURLNormalizer returns null, XWIKI-17023).

This supersedes the new URLFactory role proposed in XWIKI-24569 / PR #5855.

Use cases

#NeedTodayExample callers
N1Page URL: action, spaces, page name, query string, anchor, locale, revisionXWikiServletURLFactory#createURLXWiki#getURL (8 overloads), XWikiDocument#getURL, DocumentAccessBridge#getDocumentURL, $xwiki.getURL (about 350 uses in templates and pages), $doc.getURL (about 400)
N2Shape rules for page URLs: view action hidden (showviewaction), default page name dropped for the view action (usedefaultaction), view/<space> when the first space is named like an action, wiki/<alias>/ in path-based modefactory + XWiki#showViewAction / useDefaultAction / getServletPathsame as N1
N3Attachment URLs (download, downloadrev, viewattachrev, delattachment), with rev= added to bust caches and rid= for attachments in the recycle bincreateAttachmentURL / createAttachmentRevisionURL; these load documents and the recycle bingetAttachmentURL (about 76 callers), REST ModelFactory
N4Skin files, wiki skin files, static resources, templates; extra query parameters (cache-version…)createSkinURL, createResourceURL, createTemplateURLXWiki#getSkinFile, AbstractSkinResource, ObjectPropertyWikiResource
N5URLs of other resource types: webjars, tmp, vfs, captcha, user, authenticationper-type ResourceReferenceSerializer<X, ExtendedURL>DefaultWebJarsUrlFactory, TemporaryResourceScriptService, AuthenticationScriptService…
N6Server-relative links for the HTML of the current request, absolute when the target is on another hostfactory + getURL(URL) relativizationeverything rendered in pages
N7Absolute URLs that follow the current request (proxy headers, xwiki.url.protocol)createExternalURL, getServerURL, ExternalServletURLFactoryREST links, WebSocket, FOP, mentions, PDF export
N8Canonical absolute URLs that only trust the configuration and the wiki descriptors, since the Host header comes from the clientXWiki#getServerURL(wikiId)reset password email, RequestInitializer; should also cover mail and notification emails
N9Background threads (scheduler, mail, jobs) with no client requestdaemon branches in XWikiServletURLFactory and XWikiany URL generated in a job
N10Per-request overrides: links action, links query string (popup.vm's xpage=popup)XWikiContext#setLinksAction / setLinksQueryStringpopup.vm
N11Filesystem export (HTML zip, Office) and FOP: copy the file, then emit a relative or file: URLExportURLFactory, FileSystemURLFactory, OfficeExporterURLFactory, the filesystem format (webjars only)HtmlPackager, ExportAction, FOP
N12Session tracking: response.encodeURL, then strip jsessionidfactory normalizeURLall factory URLs
N13Encoding identical to today's: %20 in path segments, fragment encoded with ' as %27, multi-valued parametersfactory
N14Extension point: a custom XWikiURLFactory class (xwiki.urlfactory.*class)XWikiURLFactoryServiceImplcustom installs

Problems in the current modules

#ProblemFix
F1url-api depends on an implementation module (resource-default), which itself depends on the bridge (oldcore). EntityResourceReference, EntityResourceAction, SkinsResourceReference and ResourcesResourceReference live in resource-default.Move these classes to resource-api
F2url-api depends on xwiki-commons-velocity, only to encode query strings with EscapeTool.ExtendedURL encodes its own query string
F3ExtendedURL encodes path segments with URLEncoder, which is form encoding (space → +); it has no fragment; it does two jobs (wrapper of a parsed URL, and builder for serialization).See ExtendedURL below
F4Absolute URLs can't be produced: URLURLNormalizer returns null (XWIKI-17023).Location stage
F5Server URL logic exists twice in oldcore (XWiki#getServerURL, XWikiServletURLFactory#getServerURL), with different fallbacks; a third copy is in review (PR #5855).One server URL resolver in url-default
F6Serializers don't agree on what they return: webjars, tmp, vfs and captcha add the context path themselves; authentication doesn't (its callers do); user-resource returns an absolute URL obtained from the old factory.One output contract for format serializers; the caller picks relative / absolute / canonical
F7url-container is described as container code, yet two of its three classes use servlet APIs, and its pom depends on container-servlet and the servlet API. url-default depends on container-servlet (DefaultURLSecurityManager). url-container holds a contextpath+actionservletpath normalizer that url-scheme-standard overrides with the same hint at priority 900, and nothing has used it since 2016.url-default independent of the container behind one internal role; servlet code in a -servlet module; deprecate the unused normalizer and its override
F8Pass-through dispatch classes: StandardExtendedURLResourceReferenceSerializer and url-scheme-filesystem's FilesystemResourceReferenceSerializer differ only by a string; StandardExtendedURLResourceReferenceResolver only delegates to generic.A single dispatch in DefaultResourceReferenceSerializer / DefaultResourceReferenceResolver
F9Parsing and serialization read the format id from different places: resolvers use URLConfiguration, serializers use URLContextManager (per execution context).Document it: incoming requests are parsed with the configured format, serialization uses the context's format
F10Per-type serializers are looked up by the exact class of the reference, so a subclass isn't found.Walk up the class hierarchy
F11URL generation ignores the settings the URL module defines: (a) bug: url.standard.hideViewAction, documented in xwiki.properties since 7.2M1, has no effect; generation only reads xwiki.showviewaction and a per-wiki showviewaction preference; (b) path-based mode is read from xwiki.virtual.usepath when generating and from url.standard.multiwiki.isPathBased when parsing; (c) usedefaultaction has no url.standard.* equivalent; (d) the per-wiki showviewaction / usedefaultaction preferences are undocumented, aren't fields of the XWikiPreferences class, and are read for the current wiki rather than the target wiki; (e) generation hardcodes the wiki/ prefix and takes the action prefix from xwiki.servletpath / xwiki.defaultservletpath, while parsing reads url.standard.multiwiki.wikiPathPrefix and url.standard.entityPathPrefix.StandardURLConfiguration becomes the single source, with the old keys as fallbacks
F12No serializer for pages and attachments, so other modules fall back on the old factory (TODO in XWiki#getURL, DocumentUserResourceReferenceSerializer).Entity serializer in the standard format
F13Building an attachment URL loads documents, walks document history and queries the recycle bin.That decision stays in oldcore; the serializer only renders the reference it gets
F14URL output varies through two parallel mechanisms: factory subclasses chosen by context mode, and the URL format id. The HTML export uses both.One mechanism: the format id
F15Hidden coupling to oldcore: url-scheme-standard uses XWikiContext without declaring the dependency; url-default depends on oldcore for one listener.Keep oldcore access in one place (the requested wiki id) and declare it

Proposal

Each concern has one owner, and URL serialization becomes the exact inverse of parsing:

           what is addressed           how it is written in a path          where it is served
         +------------------+   +------------------------------+   +------------------------------+
caller ->| ResourceReference|-->| URL format (standard, ...)   |-->| Location (url-default)       |--> URL / link
         | (resource-api)   |   | per-type serializer + wiki   |   | context path, server URL of  |
         +------------------+   | addressing -> ExtendedURL    |   | the wiki, relative/absolute/ |
                                | (path + parameters + fragment|   | canonical                    |
                                |  + target wiki)              |   +------------------------------+
                                +------------------------------+
                 ExtendedURL encodes, and is the value passed between the stages (url-api)
  • Resource (resource-api): what is addressed. Knows nothing about URLs.
  • URL format (url-scheme-*): how a reference is written in a path, both ways. One per-type resolver and serializer pair, round-trip tested. Owns the in-path rules: action, spaces, view action, default page, wiki/<alias> prefix.
  • Location (url-default): where the result is served. Context path, the server URL of a wiki (descriptors, xwiki.home, xwiki.url.protocol, request and proxy headers, background threads), and the three outputs: relative, absolute for the current request, absolute canonical. It doesn't depend on the URL format; every format reuses it.
  • Oldcore adapter: the XWikiURLFactory implementations turn their string arguments into references, apply the per-request overrides (N10) and storage decisions (N3), call the serializers, and apply response.encodeURL (N12).

Resource API

  • Move EntityResourceReference, EntityResourceAction, SkinsResourceReference and ResourcesResourceReference from resource-default to resource-api, keeping their package names (fixes F1).
    • Binary compatible: both jars are installed together and resource-default depends on resource-api. Revapi needs a justified ignore for the removal from resource-default.
    • resource-api gains a single dependency, model-api, which only depends on commons modules: no dependency cycle, and no package split across two jars.
    • url-api then depends on resource-api only.
  • resource-default stays, holding only org.xwiki.resource.internal.* (handler managers, entity handler, the EntityResourceActionLister role that oldcore implements). Merging it into resource-api would bring the bridge, and with it rendering-api, security-authorization-api and user-api, into an -api module.

ExtendedURL (url-api)

  • Add an optional fragment: set when parsing, appended by serialize(), encoded as the factory encodes it today (N13).
  • Add an optional target wiki (WikiReference): the wiki whose URL space the path addresses. The format sets it when the wiki isn't written in the path (domain-based multiwiki), and the location stage uses it to choose the host. This mirrors parsing, where the format finds the wiki from the host.
  • Encode path segments per RFC 3986 (%20, not +), and encode the query string without EscapeTool (fixes F2, F3). This changes the URLs of webjars and tmp resources whose names contain a space or a +; servlet containers decode both forms the same way in a path.
  • All additions are @Unstable.

URL format: the standard format

  1. Serializer for page and attachment references (ResourceReferenceSerializer<EntityResourceReference, ExtendedURL>): the inverse of AbstractEntityResourceReferenceResolver, with the same special cases (N2). Locale, revision and parameters go into the query string, the anchor into the fragment. Tests check the round trip both ways; XWikiServletURLFactoryTest (38 tests) provides the expected URLs.
  2. Wiki addressing in one place: WikiReferenceExtractor (path or domain) gains its inverse. Path-based mode writes wiki/<alias>/; domain-based mode writes nothing in the path and sets the target wiki.
  3. Serializers for skins and resources (SkinsResourceReference, ResourcesResourceReference); wiki skin files are entity references with action skin. Both parsers already exist.
  4. One output contract for format serializers (fixes F6): the path from the webapp root, without the context path, plus the target wiki.
    • Format serializers are only called by the serializers callers look up (see "The interface callers use"), which apply the matching normalizer (contextpath, url or canonicalurl). Callers never call a URLNormalizer themselves.
    • Serializers following this contract are registered under a distinct hint, <format>/path (or path for every format). A serializer found only under the existing hints follows the old convention (it adds the context path itself) and is used as it is. Every extension serializer we know of in xwiki-contrib adds the context path itself, so they all keep working with no change.
    • Platform serializers moving to the new contract (webjars, tmp, vfs, captcha, authentication) keep their class and get the new hint. Their old registration (same role, old hint, adds the context path) moves to the -legacy module as a deprecated component, for code that injects it directly. This is what XWIKI-20064 did in 14.7RC1 when the tmp serializer moved from standard/tmp to the default hint.
    • Platform callers that add the context path themselves stop doing so in the same change: AuthenticationScriptService (keeps only serialize()) and DefaultResetPasswordManager (switches to the canonical URL serializer).
  5. Configuration (fixes F11): StandardURLConfiguration is the only thing generation and parsing read.
    • isViewActionHidden() reads url.standard.hideViewAction, then xwiki.showviewaction. Both default to showing the view action. The page serializer reading it fixes the bug where the documented setting has no effect: a wiki that set url.standard.hideViewAction=true starts getting URLs without /view/ (release notes).
    • New isDefaultPageNameHidden(): reads url.standard.hideDefaultPageName, then xwiki.usedefaultaction (inverted). The default hides WebHome, as today.
    • Path prefixes (url.standard.multiwiki.wikiPathPrefix, url.standard.entityPathPrefix) are read by generation too.
    • isPathBasedMultiWiki() delegates to the location stage's setting.
  6. Dispatch (fixes F8, F10). The format logic is per (scheme, resource type) and stays so; what goes away is a pass-through step in front of it.
    • DefaultResourceReferenceSerializer first looks for an optional scheme-wide serializer (hint = format id), as the resolver side already does. Otherwise it looks up the per-type serializer, walking up the reference's class hierarchy: new-contract hints first (<format>/path, then path), then the old ones (format id, then default).
    • StandardExtendedURLResourceReferenceSerializer, url-scheme-filesystem's FilesystemResourceReferenceSerializer and StandardExtendedURLResourceReferenceResolver are deleted.
    • Rule for both directions: a (scheme, type) pair is a serializer with hint <scheme> plus a resolver with hint <scheme>/<type>; leaving the scheme out makes the pair apply to every scheme. A scheme that changes a type's path also maps that path in its type resolver. Example: the existing reference format (which only parses today) could give webjars a different path by registering a webjars serializer with hint reference, a resolver with hint reference/webjars, and the mapping in its type resolver.

Location (url-default)

  • No servlet code in url-default (F7), following the guidelines discussed during the move to Jakarta (forum, forum): modules avoid container-specific APIs, and code needing servlet APIs goes in a module suffixed -servlet.
    • url-default depends only on container-api. What it needs from the request goes through one internal role, the request location: the base URL the client used (as seen through proxies), whether the request is a background-thread stub, and the context path. The container-api Request can't give these today.
    • url-container is renamed xwiki-platform-url-servlet (old name kept as an extension feature). It holds the servlet implementation of the request location, url-container's classes (same packages), and the servlet part of DefaultURLSecurityManager.
  • ServerURLResolver (internal role, url-default): the single implementation of the server URL. Two methods, because the two needs conflict:
    • getServerURL(wikiId) (N7, N9): follows the current request (proxies, xwiki.url.protocol), with the same fallbacks as today (xwiki.home, path-based mode, the descriptor, background threads).
    • getConfiguredServerURL(wikiId) (N8): only the configuration (xwiki.home, the descriptor's alias, port and secure flag, xwiki.url.protocol, the main wiki's descriptor). The request's protocol and port are only used when nothing is configured for the requested wiki.
  • Path-based mode is a deployment setting, not a format setting: it moves to url-default, keeping the key url.standard.multiwiki.isPathBased with xwiki.virtual.usepath as fallback.
  • Normalizers are the location stage's interface (existing URLNormalizer role):
    • contextpath → ExtendedURL with the context path (exists);
    • url → absolute URL for the target wiki, through getServerURL (implements XWIKI-17023);
    • canonicalurl (new) → absolute URL through getConfiguredServerURL;
    • contextpath+actionservletpath and its standard override: deprecated. It was added for temporary resources served by the Struts temp action (XWIKI-12867, XWIKI-12891); its path-based part moved to the standard override (XWIKI-12890, still open); temporary resources moved to the resource servlet in 2016 (XWIKI-10375). It also adds the current wiki, which is wrong for links to another wiki: the action mapping and the wiki prefix move into the entity serializer, which knows the target wiki.

The interface callers use

No new role: the existing ResourceReferenceSerializer role is used with two output types.

LookupOutputUse cases
ResourceReferenceSerializer<ResourceReference, ExtendedURL> (default, exists)server-relative, with the context pathN5, N6 on the same host
ResourceReferenceSerializer<ResourceReference, URL> (default, new)absolute, for the current requestN6 on another host, N7, N9
ResourceReferenceSerializer<ResourceReference, URL> hint canonical (new)absolute, canonicalN8

Each one is: format serializer → ExtendedURL → normalizer. The caller chooses the kind of URL by choosing the lookup. Normalizers stay public for code holding an ExtendedURL built by hand.

  • Reset password, mail and notification emails become one call, and stop swapping in ExternalServletURLFactory.
  • "Relative if same host" (N6) stays in the oldcore adapter, as getURL(URL) does today.

Compatibility: code that calls the default serialize() and then the contextpath normalizer, for a type whose serializer doesn't add the context path, gets the context path twice. Only the authentication type is in that case. Platform callers change in the same commit; extensions doing it need to switch to the canonical URL serializer (release notes). Outside the platform, the only normalizer we found in use is contextpath; nobody uses url, contextpath+actionservletpath or relativization.

Oldcore adapter

XWikiServletURLFactory keeps its public methods, which delegate:

  • createURL: resolve the spaces, apply linksAction / linksQueryString (N10), build an EntityResourceReference, get an absolute URL, apply encodeURL then strip jsessionid (N12).
  • createAttachmentURL: a new oldcore helper decides the attachment version (rev, rid, download vs downloadrev vs viewattachrev; N3, F13) and puts it on the reference. The serializer stays pure.
  • createSkinURL / createResourceURL: skins and resources references (N4).
  • getServerURL → ServerURLResolver#getServerURL; XWiki#getServerURL → getConfiguredServerURL.
  • getURL(URL): unchanged.

Later (phase 4): ExternalServletURLFactory becomes "absolute output"; ExportURLFactory, FileSystemURLFactory and OfficeExporterURLFactory become serializers of the filesystem format (N11, F14); xwiki.urlfactory.*class keeps working until the factory is deprecated (N14).

Phases

Each phase can ship on its own.

  1. Foundations, no behavior change: move the reference classes to resource-api; fragment, target wiki and own encoding in ExtendedURL; rename url-container to url-servlet, add the request location role, drop url-default's container-servlet dependency; remove the dispatch classes.
  2. Location: ServerURLResolver and the path-based setting; the url and canonicalurl normalizers; XWiki#getServerURL and the factory's getServerURL delegate; the two URL serializers; the output contract (new hints, legacy registrations, platform callers, reset password on the canonical serializer). PR #5855 is closed in favor of this.
  3. Format: entity serializer and wiki addressing; skins and resources serializers; configuration consolidation; round-trip tests.
  4. Adapter and exports: XWikiServletURLFactory delegates; attachment version helper; user-resource drops its fallback; export, PDF and Office move to the filesystem format; mail stops swapping factories; deprecate the XWikiURLFactory subclasses that no longer have a role.

Open questions

  • Q1. Add fragment and targetWiki to ExtendedURL (@Unstable), rather than a separate value type passed between the format and location stages?
  • Q2. Switch to %20 everywhere, which changes the URLs of webjars and tmp resources that contain a space?
  • Q3. Rename url-container to xwiki-platform-url-servlet (old name kept as an extension feature), or keep the name and document it as servlet-only?
  • Q4. Move the four reference classes from resource-default to resource-api?
  • Q5. The per-wiki showviewaction / usedefaultaction preferences (F11 d):
    • stop reading them (recommended), with a release-notes entry. They're undocumented, can't be set from the UI, a URL shape varying per wiki makes links inconsistent across a farm, and no xwiki-contrib code or page sets them. The global xwiki.cfg keys stay as fallbacks;
    • or deprecate them: honoured for a few releases with a warning, then removed.
  • Q6. Templates: a new templates resource type, or keep them in the adapter? They're static files served by the container.
  • Q7. Names of the new hints: canonical for the URL serializer and canonicalurl for the normalizer (recommended)? No known extension uses these hints. The absolute serializers stay typed on URL (ActivityPub already registers its own absolute serializer with URI output).
  • Q8. Should "relative if same host" (N6) move from the adapter into the location stage, as a URLNormalizer<String>? Recommended: not now; no extension needs it, so it can move later without breaking anyone.
  • Q9. How to tell new-contract serializers from those that add the context path?
    • a distinct hint (recommended, described above): no guessing, every existing serializer keeps working;
    • or a heuristic (the path already starts with the context path). It fails when a new-contract path starts with a segment named like the context path, e.g. the webapp deployed at /wiki in path-based mode, where the link to subwiki foo starts with wiki/foo/.

Alternatives rejected

  • A new public URLFactory role (PR #5855): a second way to create URLs next to the serializers, as a public interface over logic that is only partly ported, using XWikiContext in url-default.
  • FullExtendedURL: the authority depends on the output (request vs canonical), so it can't be fixed when the format serializes. What travels between the stages is the target wiki, not a host.
  • Server URL logic in url-scheme-standard, plus a priority-900 override of URLURLNormalizer: which wiki maps to which host is a deployment setting shared by every format, and an override with the same hint makes the behavior depend on which jars are installed (as XWIKI-12890 showed).
  • An execution-context flag "generate absolute URLs", as mail does today by swapping factories: hidden state. The caller should choose the output explicitly.
  • Format serializers returning URL directly: every serializer would have to know about proxies, descriptors and canonical URLs, which F6 already shows on a small scale.

 

Get Connected