URL Mapping

Last modified by Raphaël Jakse on 2025/02/12 17:55

cogURL Mapping is a framework to handle requests from configured prefixes.
TypeJAR
CategoryOther
Developed byUnknown
Rating
1 Votes
LicenseGNU Lesser General Public License 2.1
Success

Installable with the Extension Manager

Description

URL Mapping is a framework to handle requests from configured prefixes. The initial motivation behind this extension is that some people migrate to XWiki from another wiki solution (for example Confluence, for which we have a extension) or from a CMS and want their old links to still work (because Cool URIs don’t change). Visitors could have mails linking to the old website, or could have bookmark.

The URL Mapping extension provides a convenient way for developers to implement such redirections, and provides a convenient way for administrators to configure such implementations. 

For instance, figure a wiki freshly migrated from Confluence. Many visitors of this wiki still have Confluence links to documents in their history, bookmarks or mails, but after the migration, the documents have of course moved and are accessed through URLs shaped by XWiki instead of URL shaped by Confluence. The URL Mapping extension makes it possible to:

  • for a developer to write a set of URL mappers which will recognize URLs that look like /display/SPACE/Page title, /spaces/SPACE/pages/NNN/Page title and download/attachments/NNN/filename.ext and redirect to the right document or attachment;
  • for an administrator to configure a prefix where these URLs will be expected, so a reverse proxy can direct these old URLs to this prefix without risking to clash with XWiki URLs.
  • for an administrator to configure a redirection screen that automatically redirects after a specified delay and that tells users to update their bookmarks… or, on the contrary, to let the redirects be transparent and immediate

Targets of this framework

It targets two groups of people:

  • Administrators, who want to configure and provide redirections to their users. Usually, they will be installing a particular URL Mapping implementation and will find information here on how to configure this implementation.
  • Extension developers, who want to provide such a particular implementation.

In this documentation, we will first see how to configure the URL Mapping extension (for administrators), and then how to provide an implementation (for developers).

Configuration

URL Mapping provides a configuration mechanism. We describe how it works in this section.

Warning

This configuration mechanism is totally optional, it is up to each particular implementation to use it. Administrators should make sure to check the documentation of each implementation they use to see if it uses this configuration mechanism, and check out which name it uses.

Information

That said, the easiest and recommended way to write an implementation will make use of this configuration mechanism, it should be safe to assume that it will work with any well-behaved implementation.

Each implementation has a unique name, which you should find in its documentation. In the reminder of this section, we will consider a fictive implementation named myhandler. Replace myhandler with the actual documented name.

Here is a comprehensive example of a snippet to put in xwiki.properties:

# The prefix to use for myhandler. Assuming a /xwiki context, requests to /xwiki/myprefix/XXX will be sent to this handler.
# Without a prefix, myhandler will not be active
urlmapping.prefixhandlers.myhandler.prefix=myprefix

# Display a redirection screen for the following number of seconds.
# Special values:
#   0  (the default) skips the redirection screen altogether
#  -1  disables automatic redirections, users will have to click the target link themselves
urlmapping.prefixhandlers.myhandler.delay=0

# In the redirection screen, show this message. If left unset or empty, a generic, default message will be shown. This message will be HTML-escaped and displayed as is. There is no need to escape anything and there is no way to use any formatting.
urlmapping.prefixhandlers.myhandler.introMessage=Redirecting. Please update your bookmarks.

# If a URL is understood but the target is not found, the user will be shown the following error message. If left unset or empty, a generic, default error message will be shown. There is no need to escape anything and there is no way to use any formatting.
urlmapping.prefixhandlers.myhandler.notFoundIntroMessage=Sorry, we could not find what you are looking for.

# If set and non-empty, the following will be used as a title in the redirection screen instead of the default, generic one.
urlmapping.prefixhandlers.myhandler.title=We found your doc!

# If set and non-empty, the following will be used as a title in the redirection screen instead of the default, generic one.
# Note: it is not used in the not found screen.
urlmapping.prefixhandlers.myhandler.title=We found your doc!

# The template to use for the redirect screen. If empty, the one provided with the URL Mapping extension will be used.
urlmapping.prefixhandlers.myhandler.redirectScreenTemplate=mycustomredirectscreen.vm

# The template to use for the screen displayed when a target is not found. If empty, the one provided with the URL Mapping extension will be used.
urlmapping.prefixhandlers.myhandler.notFoundScreenTemplate=

# The redirect HTTP status to use. By default, a 302 temporary redirect status will be used.
urlmapping.prefixhandlers.myhandler.redirectHttpStatus=307
Information

Templates are rendered using com.xpn.xwiki.web.Utils#parseTemplate(String, XWikiContext), making it possible to load UI and Javascript extensions for a proper rendering of the full XWiki UI.

Information

Custom templates may decide to honor or not honor the other parameters.

In addition to the handler-specific configuration, it is possible to provide a default configuration which will apply to all handlers. It is possible to use any key that is documented earlier under urlmapping.default (except prefix: no default prefix can be configured as each handler needs its own, unique prefix).

In addition to the handler-specific configuration and the default configuration, each implementation (handler) can suggest or force configuration values. When fetching the configuration, the URL Mapping will look for a value in this order:

  1. any configuration forced by the implementation (likely unusual);
  2. the handler-specific configuration;
  3. the default configuration;
  4. the configuration suggested by the implementation.

This lets developers provide good defaults, administrator to have a fine-grained control over the configuration and developers to force values when anything else would not make sense.

Implementing a URL Mapping handler

Quick start with examples

  1. Write a mapper for each family of links you want to convert. They can be components, or simple classes. Mappers extend the AbstractURLMapper class which lets you:
    • provide one or several regular expressions that match the links. One should be enough for most cases. We advise using named groups in regular expressions: notice how clear it makes the conversion code below
    • implement a convert method that returns a resource reference to the target of the link.
    • optionally implement a getSuggestions method that returns suggestions in case convert returns null.
  2. Write a prefix handler: this is a named component that extends AbstractURLMappingPrefixHandler. The name must be unique and will be used for configuration management.

For the prefix handler:

  • Implement getMappers. It must return an array of mappers. If your mappers are components, inject them and put them in the array. If they are regular classes, put instances of these classes.
  • Optionally implement initializeConfigurationDefaults(DefaultURLMappingConfiguration configuration). This is for if you want to provide more specific defaults for this handler, while still allowing administrators to override your choices. It can be used to provide more specific messages, or a whole custom screen template.

Here are two simplified examples of mappers (where error handling has been cut down):

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;
import org.xwiki.contrib.urlmapping.AbstractURLMapper;
import org.xwiki.contrib.urlmapping.DefaultURLMappingMatch;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.resource.ResourceReference;
import org.xwiki.resource.entity.EntityResourceAction;
import org.xwiki.resource.entity.EntityResourceReference;

@Component (roles = ConfluenceAttachmentURLMapper.class)
@Singleton
public class ConfluenceAttachmentURLMapper extends AbstractURLMapper
{
   @Inject
   private ConfluencePageIdResolver confluenceIdResolver;

   public ConfluenceAttachmentURLMapper()
   {
       super("download/attachments/(?<pageId>\\d+)/(?<filename>[^?#]+)(?<params>\\?.*)?$");
   }

   @Override
   public ResourceReference convert(DefaultURLMappingMatch match)
   {
       try {
            Matcher matcher = match.getMatcher();
            String pageIdStr = matcher.group("pageId");
            String filename = URLDecoder.decode(matcher.group("filename"), StandardCharsets.UTF_8);
           long pageId = Long.parseLong(pageIdStr);
            EntityReference docRef = confluenceIdResolver.getDocumentById(pageId);
            AttachmentReference attachmentRef = new AttachmentReference(filename, new DocumentReference(docRef));
           return new EntityResourceReference(attachmentRef, EntityResourceAction.VIEW);
       } finally {
           return null;
       }
   }
}
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;
import org.xwiki.contrib.urlmapping.AbstractURLMapper;
import org.xwiki.contrib.urlmapping.DefaultURLMappingMatch;
import org.xwiki.contrib.urlmapping.suggestions.URLMappingSuggestionUtils;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.LocalDocumentReference;
import org.xwiki.rendering.block.Block;
import org.xwiki.resource.ResourceReference;
import org.xwiki.resource.entity.EntityResourceAction;
import org.xwiki.resource.entity.EntityResourceReference;
import org.xwiki.contrib.confluence.resolvers.ConfluencePageTitleResolver;

@Component (roles = ConfluenceDisplayURLMapper.class)
@Singleton
public class ConfluenceDisplayURLMapper extends AbstractURLMapper
{
   @Inject
   private ConfluencePageTitleResolver confluencePageTitleResolver;

   @Inject
   private URLMappingSuggestionUtils suggestionUtils;

   public ConfluenceDisplayURLMapper()
   {
       super("display/(?<spaceKey>[^/]+)/(?<pageTitle>[^?/#]+)(?<params>\\?.*)?$");
   }

   @Override
   public ResourceReference convert(DefaultURLMappingMatch match)
   {
       try {
            Matcher matcher = match.getMatcher();
            String spaceKey = matcher.group("spaceKey");
            String pageTitle = URLDecoder.decode(matcher.group("pageTitle"), StandardCharsets.UTF_8);
            EntityReference docRef = confluencePageTitleResolver.getDocumentByTitle(spaceKey, pageTitle);
           return new EntityResourceReference(docRef, EntityResourceAction.VIEW);
       } finally {
           return null;
       }
   }

   @Override
   protected Block getSuggestions(DefaultURLMappingMatch match)
   {
        Matcher matcher = match.getMatcher();
        String spaceKey = matcher.group("spaceKey");
        String pageTitle = URLDecoder.decode(matcher.group("pageTitle"), StandardCharsets.UTF_8);
       return suggestionUtils.getSuggestionsFromDocumentReference(new LocalDocumentReference(spaceKey, pageTitle));
   }
}

This second mapper uses URLMappingSuggestionUtils from the url-mapping-suggestions module to return suggestions from a fictive entity reference.

Here is an example of a prefix handler:

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;
import org.xwiki.contrib.urlmapping.DefaultURLMappingConfiguration;
import org.xwiki.contrib.urlmapping.URLMapper;
import org.xwiki.contrib.urlmapping.AbstractURLMappingPrefixHandler;

import static org.xwiki.contrib.urlmapping.DefaultURLMappingConfiguration.Key;

@Component
@Singleton
@Named("confluence")
public class ConfluenceURLMappingPrefixHandler extends AbstractURLMappingPrefixHandler
{
   @Inject
   private ConfluenceAttachmentURLMapper confluenceAttachmentURLMapper;

   @Inject
   private ConfluenceDisplayURLMapper confluenceDisplayURLMapper;

   @Inject
   private ConfluenceViewPageURLMapper confluenceViewPageURLMapper;

   @Override
   protected URLMapper[] getMappers()
   {
       return new URLMapper[] {
            confluenceAttachmentURLMapper,
            confluenceDisplayURLMapper,
            confluenceViewPageURLMapper
       };
   }

   @Override
   protected void initializeConfigurationDefaults(DefaultURLMappingConfiguration configuration)
   {
       // Of course, use a localization manager, don't hardcode messages like this.
       configuration.setDefault(Key.INTRO_MESSAGE, "Hello! It seems you used an old Confluence link. Update your bookmark :-)");
        configuration.setDefault(Key.NOT_FOUND_INTRO_MESSAGE, "Sorry, we could not find the document corresponding to this old Confluence link");
   }
}

The full, working examples can be found in the confluence-url-mapping module of the confluence contrib package at https://github.com/xwiki-contrib/confluence/tree/master/confluence-url-mapping/src/main/java/org/xwiki/contrib/confluence/urlmapping/internal.

Documentation

The URL Mapping framework is centered around the following interfaces.

URLMappingPrefixHandler

This represents a prefix handler that handles all the URLs under a given prefix.

  • its String getPrefix() method provides the prefix it handles
  • its URLMappingResult convert(String path, String method, HttpServletRequest request) method converts an incoming request. Returning null means that the handler didn't handle the request in the end, although it's generally better to handle the situation explicitly. See the next section.

URLMappingResult

This represents the result of a conversion:

  • its ResourceReference getResourceReference() or its String getURL() method contains the target of the conversion. If no conversion could be performed, both should return null or empty. Resource references will be converted using Wiki.getURL() and the specified action should be honored.
  • its Block getSuggestions() method optionally provides some suggestion to show in case the conversion failed.
  • its int getHTTPStatus() method provides the HTTP status to use in case a redirect is not issued and a redirection / not found screen is shown instead. 0 can be used to let the URL Mapping extension return a sensible status (404 is no target is provided, 200 if one is provided).
  • its URLMappingConfiguration getConfiguration() method returns the configuration to use for the redirection.

URLMappingConfiguration

This represents the configuration to use for a conversion. It can be static, or it can change every time a conversion is computed. It can be sourced from the wiki configuration, or totally generated at runtime. See the Configuration section for an intuition on what this interface provides (the configuration of an handler is a direct reflection of this interface), and the code for the exact details.

AbstractURLMappingPrefixHandler

This is an opinionated / "managed" take on how to handle conversions. We think that you should implement your handlers by extending this class unless you have very specific needs.

It provides:

  • automatic configuration and prefix management;
  • a conversion mechanism centered around the concept of URL mappers.

See the example above for basic usage. You can read the code to discover the protected and public methods you can override but there's not much here. You can go wild and override getConfiguration() if you want to customize the configuration management from your handler but this should not be usually necessary.

URLMapper and AbstractURLMapper

URL mappers are available to handlers extending AbstractURLMappingPrefixHandler.

They let you convert separate families of links separately instead of doing everything in the convert method. For instance, you can have a mapper handling links to spaces, a mapper handling links to attachments, and a mapper handlings links to documents.

Here's how things work:

  1. Each mapper specifies the family of links it manages through a URLMappingSpecification object returned by its getSpecification() method. Note: AbstractURLMapper conveniently lets you pass the specification to use in its constructors, either as an URLMappingSpecification object, or as a list of regex provided as String or Pattern so you don't have to implement getSpecification() yourself.
  2. When a request is to be converted by the handler, it gets matched with its mappers' specifications, in order.
  3. As soon as a specification matches, the result of this match (including the captured groups of regular expressions in a Matcher object, which also includes named groups) is passed to the corresponding mapper using its convert(URLMappingMatch) method
  4. If the mapper can convert the request, it returns a non-null result. The conversion ends: AbstractURLMappingPrefixHandler will return this result. If it can't, it returns null and the next specifications / mappers are tried.

Mappers can also provide a getSuggestions() method that will be called if the convert method return null, to provide suggestions to the user.

Tips:

  • Usually, if request matches a specification, no other mapper will handle the request. As a result, if the mapper doesn't find the target, it can still meaningfully return a non-null URLMappingResult that contains suggestions.
  • You can manage fallbacks using a catch-all mapper at the end of the list of mappers your handler provides. Catch-all mappers have a specification that match any request. This is the case when no regular expression have been provided. This can be useful for more general suggestions or a more generic link handling.
Information

With AbstractURLMapper, conversion is usually provided by implementing ResourceReference convert(DefaultURLMappingMatch match). Alternatively, if you need more control, you can override public URLMappingResult convert(URLMappingMatch match) instead.

URLMappingSpecification

  • Its Collection<String> getHandledHTTPMethods() method lets you list the HTTP methods (like "get" or "post") your mapper should be limited to. null or empty matches any HTTP method.
  • Its Pattern[] getRegexes() method lets you provide zero, one or more regular expressions to match the requests' URLs. If you don't provide any regular expression, any path will be matched.
Information

Paths don't include the configured prefix and don't start with a slash.

DefaultURLMappingSpecification additionally provides a convenient way to initialize a specification.

Prerequisites & Installation Instructions

We recommend using the Extension Manager to install this extension (Make sure that the text "Installable with the Extension Manager" is displayed at the top right location on this page to know if this extension can be installed with the Extension Manager).

You can also use the manual method which involves dropping the JAR file and all its dependencies into the WEB-INF/lib folder and restarting XWiki.

Release Notes

v0.0.5

v0.0.4

v0.0.3

v0.0.2

v0.0.1

Initial release

Get Connected