Wiki source code of Implement a URL Mapping handler
Last modified by superadmin on 2026/10/22 17:32
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | == Quick start with examples == | ||
| 2 | |||
| 3 | 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: | ||
| 4 | 1*. 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 | ||
| 5 | 1*. implement a ##convert## method that returns a resource reference to the target of the link. | ||
| 6 | 1*. optionally implement a ##getSuggestions## method that returns suggestions in case ##convert## returns ##null##. | ||
| 7 | 1. Write a prefix handler: this is a **named** component that extends ##AbstractURLMappingPrefixHandler##. The name must be unique and will be used for configuration management. | ||
| 8 | |||
| 9 | For the prefix handler: | ||
| 10 | |||
| 11 | * 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. | ||
| 12 | * 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. | ||
| 13 | |||
| 14 | Here are two simplified examples of mappers (where error handling has been cut down): | ||
| 15 | |||
| 16 | {{code language="java"}} | ||
| 17 | import java.net.URLDecoder; | ||
| 18 | import java.nio.charset.StandardCharsets; | ||
| 19 | import java.util.regex.Matcher; | ||
| 20 | |||
| 21 | import javax.inject.Inject; | ||
| 22 | import javax.inject.Singleton; | ||
| 23 | |||
| 24 | import org.xwiki.component.annotation.Component; | ||
| 25 | import org.xwiki.contrib.urlmapping.AbstractURLMapper; | ||
| 26 | import org.xwiki.contrib.urlmapping.DefaultURLMappingMatch; | ||
| 27 | import org.xwiki.model.reference.AttachmentReference; | ||
| 28 | import org.xwiki.model.reference.DocumentReference; | ||
| 29 | import org.xwiki.model.reference.EntityReference; | ||
| 30 | import org.xwiki.resource.ResourceReference; | ||
| 31 | import org.xwiki.resource.entity.EntityResourceAction; | ||
| 32 | import org.xwiki.resource.entity.EntityResourceReference; | ||
| 33 | |||
| 34 | @Component (roles = ConfluenceAttachmentURLMapper.class) | ||
| 35 | @Singleton | ||
| 36 | public class ConfluenceAttachmentURLMapper extends AbstractURLMapper | ||
| 37 | { | ||
| 38 | @Inject | ||
| 39 | private ConfluencePageIdResolver confluenceIdResolver; | ||
| 40 | |||
| 41 | public ConfluenceAttachmentURLMapper() | ||
| 42 | { | ||
| 43 | super("download/attachments/(?<pageId>\\d+)/(?<filename>[^?#]+)(?<params>\\?.*)?$"); | ||
| 44 | } | ||
| 45 | |||
| 46 | @Override | ||
| 47 | public ResourceReference convert(DefaultURLMappingMatch match) | ||
| 48 | { | ||
| 49 | try { | ||
| 50 | Matcher matcher = match.getMatcher(); | ||
| 51 | String pageIdStr = matcher.group("pageId"); | ||
| 52 | String filename = URLDecoder.decode(matcher.group("filename"), StandardCharsets.UTF_8); | ||
| 53 | long pageId = Long.parseLong(pageIdStr); | ||
| 54 | EntityReference docRef = confluenceIdResolver.getDocumentById(pageId); | ||
| 55 | AttachmentReference attachmentRef = new AttachmentReference(filename, new DocumentReference(docRef)); | ||
| 56 | return new EntityResourceReference(attachmentRef, EntityResourceAction.VIEW); | ||
| 57 | } finally { | ||
| 58 | return null; | ||
| 59 | } | ||
| 60 | } | ||
| 61 | } | ||
| 62 | {{/code}} | ||
| 63 | |||
| 64 | {{code language="java"}} | ||
| 65 | import java.net.URLDecoder; | ||
| 66 | import java.nio.charset.StandardCharsets; | ||
| 67 | import java.util.regex.Matcher; | ||
| 68 | |||
| 69 | import javax.inject.Inject; | ||
| 70 | import javax.inject.Singleton; | ||
| 71 | |||
| 72 | import org.xwiki.component.annotation.Component; | ||
| 73 | import org.xwiki.contrib.urlmapping.AbstractURLMapper; | ||
| 74 | import org.xwiki.contrib.urlmapping.DefaultURLMappingMatch; | ||
| 75 | import org.xwiki.contrib.urlmapping.suggestions.URLMappingSuggestionUtils; | ||
| 76 | import org.xwiki.model.reference.EntityReference; | ||
| 77 | import org.xwiki.model.reference.LocalDocumentReference; | ||
| 78 | import org.xwiki.rendering.block.Block; | ||
| 79 | import org.xwiki.resource.ResourceReference; | ||
| 80 | import org.xwiki.resource.entity.EntityResourceAction; | ||
| 81 | import org.xwiki.resource.entity.EntityResourceReference; | ||
| 82 | import org.xwiki.contrib.confluence.resolvers.ConfluencePageTitleResolver; | ||
| 83 | |||
| 84 | @Component (roles = ConfluenceDisplayURLMapper.class) | ||
| 85 | @Singleton | ||
| 86 | public class ConfluenceDisplayURLMapper extends AbstractURLMapper | ||
| 87 | { | ||
| 88 | @Inject | ||
| 89 | private ConfluencePageTitleResolver confluencePageTitleResolver; | ||
| 90 | |||
| 91 | @Inject | ||
| 92 | private URLMappingSuggestionUtils suggestionUtils; | ||
| 93 | |||
| 94 | public ConfluenceDisplayURLMapper() | ||
| 95 | { | ||
| 96 | super("display/(?<spaceKey>[^/]+)/(?<pageTitle>[^?/#]+)(?<params>\\?.*)?$"); | ||
| 97 | } | ||
| 98 | |||
| 99 | @Override | ||
| 100 | public ResourceReference convert(DefaultURLMappingMatch match) | ||
| 101 | { | ||
| 102 | try { | ||
| 103 | Matcher matcher = match.getMatcher(); | ||
| 104 | String spaceKey = matcher.group("spaceKey"); | ||
| 105 | String pageTitle = URLDecoder.decode(matcher.group("pageTitle"), StandardCharsets.UTF_8); | ||
| 106 | EntityReference docRef = confluencePageTitleResolver.getDocumentByTitle(spaceKey, pageTitle); | ||
| 107 | return new EntityResourceReference(docRef, EntityResourceAction.VIEW); | ||
| 108 | } finally { | ||
| 109 | return null; | ||
| 110 | } | ||
| 111 | } | ||
| 112 | |||
| 113 | @Override | ||
| 114 | protected Block getSuggestions(DefaultURLMappingMatch match) | ||
| 115 | { | ||
| 116 | Matcher matcher = match.getMatcher(); | ||
| 117 | String spaceKey = matcher.group("spaceKey"); | ||
| 118 | String pageTitle = URLDecoder.decode(matcher.group("pageTitle"), StandardCharsets.UTF_8); | ||
| 119 | return suggestionUtils.getSuggestionsFromDocumentReference(new LocalDocumentReference(spaceKey, pageTitle)); | ||
| 120 | } | ||
| 121 | } | ||
| 122 | {{/code}} | ||
| 123 | |||
| 124 | This second mapper uses ##URLMappingSuggestionUtils## from the ##url-mapping-suggestions## module to return suggestions from a fictive entity reference. | ||
| 125 | |||
| 126 | Here is an example of a prefix handler: | ||
| 127 | |||
| 128 | {{code language="java"}} | ||
| 129 | import javax.inject.Inject; | ||
| 130 | import javax.inject.Named; | ||
| 131 | import javax.inject.Singleton; | ||
| 132 | |||
| 133 | import org.xwiki.component.annotation.Component; | ||
| 134 | import org.xwiki.contrib.urlmapping.DefaultURLMappingConfiguration; | ||
| 135 | import org.xwiki.contrib.urlmapping.URLMapper; | ||
| 136 | import org.xwiki.contrib.urlmapping.AbstractURLMappingPrefixHandler; | ||
| 137 | |||
| 138 | import static org.xwiki.contrib.urlmapping.DefaultURLMappingConfiguration.Key; | ||
| 139 | |||
| 140 | @Component | ||
| 141 | @Singleton | ||
| 142 | @Named("confluence") | ||
| 143 | public class ConfluenceURLMappingPrefixHandler extends AbstractURLMappingPrefixHandler | ||
| 144 | { | ||
| 145 | @Inject | ||
| 146 | private ConfluenceAttachmentURLMapper confluenceAttachmentURLMapper; | ||
| 147 | |||
| 148 | @Inject | ||
| 149 | private ConfluenceDisplayURLMapper confluenceDisplayURLMapper; | ||
| 150 | |||
| 151 | @Inject | ||
| 152 | private ConfluenceViewPageURLMapper confluenceViewPageURLMapper; | ||
| 153 | |||
| 154 | @Override | ||
| 155 | protected URLMapper[] getMappers() | ||
| 156 | { | ||
| 157 | return new URLMapper[] { | ||
| 158 | confluenceAttachmentURLMapper, | ||
| 159 | confluenceDisplayURLMapper, | ||
| 160 | confluenceViewPageURLMapper | ||
| 161 | }; | ||
| 162 | } | ||
| 163 | |||
| 164 | @Override | ||
| 165 | protected void initializeConfigurationDefaults(DefaultURLMappingConfiguration configuration) | ||
| 166 | { | ||
| 167 | // Of course, use a localization manager, don't hardcode messages like this. | ||
| 168 | configuration.setDefault(Key.INTRO_MESSAGE, "Hello! It seems you used an old Confluence link. Update your bookmark :-)"); | ||
| 169 | configuration.setDefault(Key.NOT_FOUND_INTRO_MESSAGE, "Sorry, we could not find the document corresponding to this old Confluence link"); | ||
| 170 | } | ||
| 171 | } | ||
| 172 | {{/code}} | ||
| 173 | |||
| 174 | The full, working examples can be found in the [[##confluence-url-mapping##>>Extension.Confluence.URL Mapping]] module of the ##confluence## contrib package at {{scm user="xwiki-contrib" project="confluence" path="confluence-url-mapping/src/main/java/org/xwiki/contrib/confluence/urlmapping/internal"/}}. | ||
| 175 | |||
| 176 | == Documentation == | ||
| 177 | |||
| 178 | The URL Mapping framework is centered around the following interfaces. | ||
| 179 | |||
| 180 | === ##URLMappingPrefixHandler## === | ||
| 181 | |||
| 182 | This represents a prefix handler that handles all the URLs under a given prefix. | ||
| 183 | |||
| 184 | * its ##String getPrefix()## method provides the prefix it handles | ||
| 185 | * 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. | ||
| 186 | |||
| 187 | === ##URLMappingResult## === | ||
| 188 | |||
| 189 | This represents the result of a conversion: | ||
| 190 | |||
| 191 | * 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. | ||
| 192 | * its ##Block getSuggestions()## method optionally provides some suggestion to show in case the conversion failed. | ||
| 193 | * 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). | ||
| 194 | * its ##URLMappingConfiguration getConfiguration()## method returns the configuration to use for the redirection. | ||
| 195 | |||
| 196 | === ##URLMappingConfiguration## === | ||
| 197 | |||
| 198 | 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. | ||
| 199 | |||
| 200 | === ##AbstractURLMappingPrefixHandler## === | ||
| 201 | |||
| 202 | 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. | ||
| 203 | |||
| 204 | It provides: | ||
| 205 | |||
| 206 | * automatic configuration and prefix management; | ||
| 207 | * a conversion mechanism centered around the concept of URL mappers. | ||
| 208 | |||
| 209 | 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. | ||
| 210 | |||
| 211 | === ##URLMapper## and ##AbstractURLMapper## === | ||
| 212 | |||
| 213 | URL mappers are available to handlers extending ##AbstractURLMappingPrefixHandler##. | ||
| 214 | |||
| 215 | 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. | ||
| 216 | |||
| 217 | Here's how things work: | ||
| 218 | |||
| 219 | 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. | ||
| 220 | 1. When a request is to be converted by the handler, it gets matched with its mappers' specifications, in order. | ||
| 221 | 1. As soon as a specification matches, the result of this match (including the captured groups of regular expressions in a [[##Matcher##>>https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/regex/Matcher.html]] object, which also includes named groups) is passed to the corresponding mapper using its ##convert(URLMappingMatch)## method | ||
| 222 | 1. 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. | ||
| 223 | |||
| 224 | Mappers can also provide a ##getSuggestions()## method that will be called if the ##convert## method return null, to provide suggestions to the user. | ||
| 225 | |||
| 226 | Tips: | ||
| 227 | |||
| 228 | * 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. | ||
| 229 | * 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. | ||
| 230 | |||
| 231 | {{info}} | ||
| 232 | 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. | ||
| 233 | {{/info}} | ||
| 234 | |||
| 235 | === ##URLMappingSpecification## === | ||
| 236 | |||
| 237 | * 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. | ||
| 238 | |||
| 239 | * 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. | ||
| 240 | |||
| 241 | {{info}} | ||
| 242 | Paths don't include the configured prefix and don't start with a slash. | ||
| 243 | {{/info}} | ||
| 244 | |||
| 245 | ##DefaultURLMappingSpecification## additionally provides a convenient way to initialize a specification. |