The solution is composed of several parts. The two central parts are the URLComposer interface and the extended PropertyPlaceholderConfigurer (URLCompositionConfigurer).
Here is the URLComposer interface:
Code:
package ...;
import java.net.URL;
import java.net.MalformedURLException;
/**
* Composes relative URLs so that they point to an absolute destination. This
* is most useful in client applications where the client has general
* knowledge of a file or service (for example, the client code might know it
* wants the "service/security/SecurityEditor" service), but does not or
* should not have specific knowledge of where that service is located
* (http://www.mysite.com/service/security/SecurityEditor). To make
* the application more flexible (so that, for example, you can easily move
* the location of the service in the future), this interface centralizes
* the actual physical location in one place.
*/
public interface URLComposer
{
public URL composeURL(String relativeURL) throws MalformedURLException;
public String composeURLAsString(String relativeURL) throws MalformedURLException;
}
Note that I've removed our specific package names from all source code. Next is URLCompositionConfigurer:
Code:
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.BeansException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Properties;
import java.net.MalformedURLException;
import ...URLComposer;
/**
* Allows URLs in the form of <code>${composerName/path}</code> where
* 'composerName' is the name of a bean registered in the ApplicationContext
* that implements the {@link ...URLComposer URLComposer}
* interface and 'path' is the relative portion of the path to be composed.
* The final composition (absolute URL) will then be substituted for the
* property value. This class extends PropertyPlaceholderConfigurer and
* if the placeholder ${...} does not contain a composition URL, then this
* class will delegate to PropertyPlaceholderConfigurer, allowing this class
* to be used both as a PropertyPlaceholderConfigurer and a
* URLCompositionConfigurer.
*/
public class URLCompositionConfigurer extends PropertyPlaceholderConfigurer
{
private static final Log log = LogFactory.getLog(URLCompositionConfigurer.class);
private ConfigurableListableBeanFactory beanFactory;
public URLCompositionConfigurer()
{
}
//
// METHODS FROM CLASS PropertyPlaceholderConfigurer
//
protected void processProperties(final ConfigurableListableBeanFactory beanFactory, final Properties props) throws BeansException
{
this.beanFactory = beanFactory;
super.processProperties(beanFactory, props);
}
protected String resolvePlaceholder(final String ph, final Properties props)
{
if(ph != null && ph.length() > 0) {
final int idxs = ph.indexOf('/');
if(idxs != -1 && idxs < (ph.length() - 1)) {
if(log.isDebugEnabled()) log.debug(">> FOUND suspected URL composition placeholder in Spring config: '" + ph + "'");
final int idxMarker = ph.indexOf("$[");
final int idxEndMarker = ph.indexOf("]");
final String placeholder;
final String pre;
final String post;
final int idxSlash;
if(idxMarker != -1 && idxEndMarker != -1 && idxEndMarker > idxMarker) {
final String innerComposer = ph.substring(idxMarker + 2, idxEndMarker);
final int idxInnerSlash = innerComposer.indexOf('/');
if(idxInnerSlash != -1 && idxInnerSlash < (innerComposer.length() - 1)) {
placeholder = innerComposer;
idxSlash = idxInnerSlash;
if(idxMarker > 0) {
pre = ph.substring(0, idxMarker);
} else {
pre = "";
}
if(idxEndMarker < (ph.length() - 1)) {
post = ph.substring(idxEndMarker + 1);
} else {
post = "";
}
} else {
placeholder = ph;
idxSlash = idxs;
pre = "";
post = "";
}
} else {
placeholder = ph;
idxSlash = idxs;
pre = "";
post = "";
}
final String composer = placeholder.substring(0, idxSlash);
if(log.isDebugEnabled()) log.debug(">> LOOKING FOR composer '" + composer + "'...");
if(composer != null && composer.length() > 0 &&
this.beanFactory.containsBean(composer)) {
final URLComposer urlComposer = (URLComposer)beanFactory.getBean(composer, URLComposer.class);
final String path = placeholder.substring(idxSlash + 1);
if(log.isDebugEnabled()) log.debug(">> Found composer (" + urlComposer + ") - invoking with path '" + path + "'");
try {
if(!log.isDebugEnabled()) {
return pre + urlComposer.composeURLAsString(path) + post;
} else {
final String ret = pre + urlComposer.composeURLAsString(path) + post;
log.debug(">> Returning composed URL: '" + ret + "'");
return ret;
}
} catch(MalformedURLException e) {
throw new BeanDefinitionStoreException("URL composition property is malformed: placeholder='" + placeholder + "'", e);
}
}
}
}
return super.resolvePlaceholder(ph, props);
}
}
Now we just need some URLComposers to make our life easier. The least elegant part of this solution, and one that I'd like to revisit sometime, though I haven't had the time yet, is how to initially set the base URL(s). Feel free to rework this if you see fit. Anyway, here is our first URLComposer:
Code:
import org.springframework.beans.factory.BeanNameAware;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.Map;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
/**
* Converts relative URLs to absolute URLs by adding them to a base URL.
*/
public class BaseURLComposer implements URLComposer, BeanNameAware
{
private static final Log log = LogFactory.getLog(BaseURLComposer.class);
private static final Map defaultBaseURLs = new ConcurrentHashMap();
private static URL defaultBaseURL;
private URL baseURL;
private String beanName;
public static URL getDefaultBaseURL()
{
return defaultBaseURL;
}
public static void setDefaultBaseURL(final URL defaultBaseURL)
{
BaseURLComposer.defaultBaseURL = defaultBaseURL;
}
public static URL getDefaultBaseURL(final String urlLabel)
{
return (URL)defaultBaseURLs.get(urlLabel);
}
public static void setDefaultBaseURL(final String urlLabel, final URL defaultBaseURL)
{
defaultBaseURLs.put(urlLabel, defaultBaseURL);
}
public URL getBaseURL()
{
if(this.baseURL == null) {
if(log.isDebugEnabled()) log.debug(">> Fetching default base URL for '" + this.beanName + "'");
if(this.beanName != null) {
final URL check = getDefaultBaseURL(this.beanName);
if(check != null) {
if(log.isDebugEnabled()) log.debug(">> Fetched labeled default base URL '" + check + "' for label '" + this.beanName + "'");
return check;
}
}
if(log.isDebugEnabled()) log.debug(">> Did not find labeled default base URL for '" + this.beanName + "' - returning default '" + defaultBaseURL + "'");
return defaultBaseURL;
}
return this.baseURL;
}
public void setBaseURL(final URL baseURL)
{
this.baseURL = baseURL;
}
public String getBeanName()
{
return this.beanName;
}
//
// METHODS FROM INTERFACE URLComposer
//
public URL composeURL(final String relativeURL) throws MalformedURLException
{
return new URL(getBaseURL(), relativeURL);
}
public String composeURLAsString(final String relativeURL) throws MalformedURLException
{
return composeURL(relativeURL).toString();
}
//
// METHODS FROM INTERFACE BeanNameAware
//
public void setBeanName(final String name)
{
if(log.isDebugEnabled()) log.debug("-- Setting bean name for BaseURLComposer to '" + name + "'");
this.beanName = name;
}
}
This bean allows you to set a "base" URL that will then be used to turn any relative URLs into absolute URLs. A couple of strategies are provided by this bean. It allows you to set a static "defaultBaseURL" (via the static 'setDefaultBaseURL' method). You would do this in your main method before parsing your Spring config. This bean also allows you to set different static 'defaultBaseURLs' by name, via the static method 'setDefaultBaseURL(String urlLabel, URL defaultBaseURL)'. The trick here is that the label you assign to the base URL must match the name of the bean in your application context that creates the BaseURLComposer. Here is an example. Say I have these two beans declared in my application context:
Code:
<bean id="siteUrlComposer" name="baseUrlComposer" class="...BaseURLComposer"/>
<bean id="codeUrlComposer" name="rcpUrlComposer" class="...BaseURLComposer"/>
I can now set the base URLs for these two beans before loading the application context (in my main method) like this:
Code:
public static final String BEAN_SITE_URL_COMPOSER = "siteUrlComposer";
public static final String BEAN_CODE_URL_COMPOSER = "codeUrlComposer";
...
BaseURLComposer.setDefaultBaseURL(BEAN_SITE_URL_COMPOSER, baseSiteURL);
BaseURLComposer.setDefaultBaseURL(BEAN_CODE_URL_COMPOSER, baseCodeURL);
At this point we can declare our URLCompositionConfigurer in the application context:
Code:
<!-- This bean will automatically replace URL parameters in the form of -->
<!-- ${composerName/path} with a URL composed by invoking the named -->
<!-- composer with the given path. -->
<bean class="...URLCompositionConfigurer"/>
And then we are free to do this sort of thing:
Code:
<bean id="remoteAuthenticationManager" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
<property name="serviceUrl"><value>${baseUrlComposer/service/Auth}</value></property>
<property name="serviceInterface"><value>net.sf.acegisecurity.providers.rcp.RemoteAuthenticationManager</value></property>
</bean>
However, we found that we kept repeating ourselves, as we ended up with a lot of URLs in the form of ${baseUrlComposer/service/...}, so we created another URL composer to avoid having to repeat ourselves and to allow us to move the "service" path around easily if we needed to in the future. Here is ContextBasedURLComposer:
Code:
import java.net.URL;
import java.net.MalformedURLException;
/**
* Allows URLs to be composed from another URLComposer with an additional
* relative path. For example, say you have a relative URL
* 'security/SecurityEditor' and a BaseURLComposer that places all relative
* URLs in the 'http://www.mysite.com'. If you used the BaseURLComposer
* alone, then you would end up with
* 'http://www.mysite.com/security/SecurityEditor'. However, if you
* want the relative path to have additional path information but cannot
* modify your original BaseURLComposer, you can use a ContextBasedURLComposer.
* The ContextBasedURLComposer allows you to specify a "context" URLComposer
* and a contextRoot. Using the example above, you could set your
* context URLComposer to the BaseURLComposer and set your contextRoot to
* 'myapp/services'. Then, when you pass this new composer
* 'security/SecurityEditor', it would call the context composer in order
* to compose its own 'myapp/services', which would become
* 'http://www.mysite.com/myapp/services' and then add the
* passed in relative URL with the final result being
* 'http://www.mysite.com/myapp/services/security/SecurityEditor'.
* The purpose of this is to keep URLs embedded in configuration as
* specific to their domain as possible (the security service proxy should
* only know it wants 'security/SecurityEditor' - it shouldn't contain any
* knowledge of any other structures in the system), which will allow these
* services to be moved or clustered in the future without having to
* specially generate or modify the configuration files.
*/
public class ContextBasedURLComposer implements URLComposer
{
private URLComposer contextComposer;
private String contextRoot;
public URLComposer getContextComposer()
{
return contextComposer;
}
public void setContextComposer(final URLComposer contextComposer)
{
this.contextComposer = contextComposer;
}
public String getContextRoot()
{
return contextRoot;
}
public void setContextRoot(final String contextRoot)
{
this.contextRoot = contextRoot;
}
//
// METHODS FROM INTERFACE URLComposer
//
public URL composeURL(final String relativeURL) throws MalformedURLException
{
return new URL(getContextComposer().composeURL(getContextRoot()), relativeURL);
}
public String composeURLAsString(String relativeURL) throws MalformedURLException
{
return composeURL(relativeURL).toString();
}
}
We then defined an additional bean in our application context:
Code:
<bean id="serviceUrlComposer" class="...ContextBasedURLComposer">
<property name="contextComposer"><ref bean="baseUrlComposer"/></property>
<property name="contextRoot"><value>service/</value></property>
</bean>
Now instead of ${baseUrlComposer/service/Auth} we use ${serviceUrlComposer}/Auth.
It's still up to you to figure out the "base" URL in your main method and then set it via the static methods in BaseURLComposer before loading your application context. If you have any other questions, let me know.
- Andy