Implement a URL Mapping handler
Steps
Quick start with examples
- 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.
- 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:
- 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.
- When a request is to be converted by the handler, it gets matched with its mappers' specifications, in order.
- 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
- 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.
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.
DefaultURLMappingSpecification additionally provides a convenient way to initialize a specification.