Announcement Announcement Module
Collapse
No announcement yet.
URL parameters Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • URL parameters

    I ran into an issue that I believe is more specific to rich clients (though I could be wrong). We had several beans defined in our spring config on the client side that had URL (or String URL) properties. In our case, these URLs pointed back to the web server. We couldn't hard code the URLs (not even in a properties file) as the base URL can dynamically change based on how the application is launched. Here is an example:
    Code:
    base URL = http://myserver.com/dynamicportion/
    RPC services = base URL + services/
    The "dynamicportion" of the base URL changes, well, dynamically depending on who launches the application. Now say I expose a remote authentication service on http://myserver.com/dynamicportion/services/auth, so I want to define a bean on the client side like this:
    Code:
    	<bean id="remoteAuthenticationManager" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
    		<property name="serviceUrl"><value>http&#58;//myserver.com/dynamicportion/services/auth</value></property>
    		<property name="serviceInterface"><value>net.sf.acegisecurity.providers.rcp.RemoteAuthenticationManager</value></property>
    	</bean>
    Of course, I can't use this bean as-is since the dynamicportion is dynamic. At first we just skirted the issue by using a .jsp page to dynamically generate the config file and then hosted the Spring config on the web server (instead of inside the application jar). But this had a number of disadvantages, so we extended PropertyPlaceholderConfigurer and invented a "URL composition" system. Basically it allows us to do something like this:
    Code:
    	<bean id="remoteAuthenticationManager" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
    		<property name="serviceUrl"><value>$&#123;serviceUrlComposer/auth&#125;</value></property>
    		<property name="serviceInterface"><value>net.sf.acegisecurity.providers.rcp.RemoteAuthenticationManager</value></property>
    	</bean>
    Our PropertyPlacehodlerConfigurer sees the '/' in the name, and so looks up a bean in the BeanFactory with the same name as the left most path element (in this case, "serviceUrlComposer"). The bean must implement our own custom "URLComposer" interface. That bean then gets passed the remaining path found in the property placeholder and the resulting URL is used as the property value.
    I should note that In the end everything isn't automatic. Somewhere somehow you must tell the URL composers what the dynamic portions of the URL are. We are currently doing this in the "main" method of our rich client application (it will determine the base URL and then manually set a property on the URL composer found in the application context).
    I'd be willing to contribute this code to the spring-rich sandbox if it is considered useful to others.

    - Andy

  • #2
    Andy, can you possibly share your code as i have exact same problem. I am surprised that no one else has this issue?

    Amad

    Comment


    • #3
      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 &#40;for example, the client code might know it
       * wants the "service/security/SecurityEditor" service&#41;, but does not or
       * should not have specific knowledge of where that service is located
       * &#40;http&#58;//www.mysite.com/service/security/SecurityEditor&#41;.  To make
       * the application more flexible &#40;so that, for example, you can easily move
       * the location of the service in the future&#41;, this interface centralizes
       * the actual physical location in one place.
       */
      public interface URLComposer
      &#123;
        public URL composeURL&#40;String relativeURL&#41; throws MalformedURLException;
        public String composeURLAsString&#40;String relativeURL&#41; throws MalformedURLException;
      &#125;
      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>$&#123;composerName/path&#125;</code> where
       * 'composerName' is the name of a bean registered in the ApplicationContext
       * that implements the &#123;@link ...URLComposer URLComposer&#125;
       * interface and 'path' is the relative portion of the path to be composed.
       * The final composition &#40;absolute URL&#41; will then be substituted for the
       * property value.  This class extends PropertyPlaceholderConfigurer and
       * if the placeholder $&#123;...&#125; 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
      &#123;
        private static final Log log = LogFactory.getLog&#40;URLCompositionConfigurer.class&#41;;
      
        private ConfigurableListableBeanFactory beanFactory;
      
        public URLCompositionConfigurer&#40;&#41;
        &#123;
        &#125;
      
      
      
      
        //
        // METHODS FROM CLASS PropertyPlaceholderConfigurer
        //
      
        protected void processProperties&#40;final ConfigurableListableBeanFactory beanFactory, final Properties props&#41; throws BeansException
        &#123;
          this.beanFactory = beanFactory;
          super.processProperties&#40;beanFactory, props&#41;;
        &#125;
      
        protected String resolvePlaceholder&#40;final String ph, final Properties props&#41;
        &#123;
          if&#40;ph != null && ph.length&#40;&#41; > 0&#41; &#123;
            final int idxs = ph.indexOf&#40;'/'&#41;;
            if&#40;idxs != -1 && idxs < &#40;ph.length&#40;&#41; - 1&#41;&#41; &#123;
              if&#40;log.isDebugEnabled&#40;&#41;&#41; log.debug&#40;">> FOUND suspected URL composition placeholder in Spring config&#58; '" + ph + "'"&#41;;
              final int idxMarker = ph.indexOf&#40;"$&#91;"&#41;;
              final int idxEndMarker = ph.indexOf&#40;"&#93;"&#41;;
      
              final String placeholder;
              final String pre;
              final String post;
              final int idxSlash;
      
              if&#40;idxMarker != -1 && idxEndMarker != -1 && idxEndMarker > idxMarker&#41; &#123;
                final String innerComposer = ph.substring&#40;idxMarker + 2, idxEndMarker&#41;;
                final int idxInnerSlash = innerComposer.indexOf&#40;'/'&#41;;
                if&#40;idxInnerSlash != -1 && idxInnerSlash < &#40;innerComposer.length&#40;&#41; - 1&#41;&#41; &#123;
                  placeholder = innerComposer;
                  idxSlash = idxInnerSlash;
                  if&#40;idxMarker > 0&#41; &#123;
                    pre = ph.substring&#40;0, idxMarker&#41;;
                  &#125; else &#123;
                    pre = "";
                  &#125;
                  if&#40;idxEndMarker < &#40;ph.length&#40;&#41; - 1&#41;&#41; &#123;
                    post = ph.substring&#40;idxEndMarker + 1&#41;;
                  &#125; else &#123;
                    post = "";
                  &#125;
                &#125; else &#123;
                  placeholder = ph;
                  idxSlash = idxs;
                  pre = "";
                  post = "";
                &#125;
              &#125; else &#123;
                placeholder = ph;
                idxSlash = idxs;
                pre = "";
                post = "";
              &#125;
      
      
              final String composer = placeholder.substring&#40;0, idxSlash&#41;;
              if&#40;log.isDebugEnabled&#40;&#41;&#41; log.debug&#40;">> LOOKING FOR composer '" + composer + "'..."&#41;;
              if&#40;composer != null && composer.length&#40;&#41; > 0 &&
                 this.beanFactory.containsBean&#40;composer&#41;&#41; &#123;
                final URLComposer urlComposer = &#40;URLComposer&#41;beanFactory.getBean&#40;composer, URLComposer.class&#41;;
                final String path = placeholder.substring&#40;idxSlash + 1&#41;;
                if&#40;log.isDebugEnabled&#40;&#41;&#41; log.debug&#40;">> Found composer &#40;" + urlComposer + "&#41; - invoking with path '" + path + "'"&#41;;
                try &#123;
                  if&#40;!log.isDebugEnabled&#40;&#41;&#41; &#123;
                    return pre + urlComposer.composeURLAsString&#40;path&#41; + post;
                  &#125; else &#123;
                    final String ret = pre + urlComposer.composeURLAsString&#40;path&#41; + post;
                    log.debug&#40;">> Returning composed URL&#58; '" + ret + "'"&#41;;
                    return ret;
                  &#125;
                &#125; catch&#40;MalformedURLException e&#41; &#123;
                  throw new BeanDefinitionStoreException&#40;"URL composition property is malformed&#58; placeholder='" + placeholder + "'", e&#41;;
                &#125;
              &#125;
            &#125;
          &#125;
          return super.resolvePlaceholder&#40;ph, props&#41;;
        &#125;
      &#125;
      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
      &#123;
        private static final Log log = LogFactory.getLog&#40;BaseURLComposer.class&#41;;
      
        private static final Map defaultBaseURLs = new ConcurrentHashMap&#40;&#41;;
        private static URL defaultBaseURL;
      
        private URL baseURL;
        private String beanName;
      
        public static URL getDefaultBaseURL&#40;&#41;
        &#123;
          return defaultBaseURL;
        &#125;
      
        public static void setDefaultBaseURL&#40;final URL defaultBaseURL&#41;
        &#123;
          BaseURLComposer.defaultBaseURL = defaultBaseURL;
        &#125;
      
        public static URL getDefaultBaseURL&#40;final String urlLabel&#41;
        &#123;
          return &#40;URL&#41;defaultBaseURLs.get&#40;urlLabel&#41;;
        &#125;
      
        public static void setDefaultBaseURL&#40;final String urlLabel, final URL defaultBaseURL&#41;
        &#123;
          defaultBaseURLs.put&#40;urlLabel, defaultBaseURL&#41;;
        &#125;
      
        public URL getBaseURL&#40;&#41;
        &#123;
          if&#40;this.baseURL == null&#41; &#123;
            if&#40;log.isDebugEnabled&#40;&#41;&#41; log.debug&#40;">> Fetching default base URL for '" + this.beanName + "'"&#41;;
            if&#40;this.beanName != null&#41; &#123;
              final URL check = getDefaultBaseURL&#40;this.beanName&#41;;
              if&#40;check != null&#41; &#123;
                if&#40;log.isDebugEnabled&#40;&#41;&#41; log.debug&#40;">> Fetched labeled default base URL '" + check + "' for label '" + this.beanName + "'"&#41;;
                return check;
              &#125;
            &#125;
            if&#40;log.isDebugEnabled&#40;&#41;&#41; log.debug&#40;">> Did not find labeled default base URL for '" + this.beanName + "' - returning default '" + defaultBaseURL + "'"&#41;;
            return defaultBaseURL;
          &#125;
      
          return this.baseURL;
        &#125;
      
        public void setBaseURL&#40;final URL baseURL&#41;
        &#123;
          this.baseURL = baseURL;
        &#125;
      
        public String getBeanName&#40;&#41;
        &#123;
          return this.beanName;
        &#125;
      
      
        //
        // METHODS FROM INTERFACE URLComposer
        //
      
        public URL composeURL&#40;final String relativeURL&#41; throws MalformedURLException
        &#123;
          return new URL&#40;getBaseURL&#40;&#41;, relativeURL&#41;;
        &#125;
      
        public String composeURLAsString&#40;final String relativeURL&#41; throws MalformedURLException
        &#123;
          return composeURL&#40;relativeURL&#41;.toString&#40;&#41;;
        &#125;
      
      
        //
        // METHODS FROM INTERFACE BeanNameAware
        //
      
        public void setBeanName&#40;final String name&#41;
        &#123;
          if&#40;log.isDebugEnabled&#40;&#41;&#41; log.debug&#40;"-- Setting bean name for BaseURLComposer to '" + name + "'"&#41;;
          this.beanName = name;
        &#125;
      &#125;
      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&#40;BEAN_SITE_URL_COMPOSER, baseSiteURL&#41;;
              BaseURLComposer.setDefaultBaseURL&#40;BEAN_CODE_URL_COMPOSER, baseCodeURL&#41;;
      At this point we can declare our URLCompositionConfigurer in the application context:
      Code:
        <!-- This bean will automatically replace URL parameters in the form of -->
        <!-- $&#123;composerName/path&#125; 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>$&#123;baseUrlComposer/service/Auth&#125;</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&#58;//www.mysite.com'.  If you used the BaseURLComposer
       * alone, then you would end up with
       * 'http&#58;//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&#58;//www.mysite.com/myapp/services' and then add the
       * passed in relative URL with the final result being
       * 'http&#58;//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 &#40;the security service proxy should
       * only know it wants 'security/SecurityEditor' - it shouldn't contain any
       * knowledge of any other structures in the system&#41;, 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
      &#123;
        private URLComposer contextComposer;
        private String contextRoot;
      
        public URLComposer getContextComposer&#40;&#41;
        &#123;
          return contextComposer;
        &#125;
      
        public void setContextComposer&#40;final URLComposer contextComposer&#41;
        &#123;
          this.contextComposer = contextComposer;
        &#125;
      
        public String getContextRoot&#40;&#41;
        &#123;
          return contextRoot;
        &#125;
      
        public void setContextRoot&#40;final String contextRoot&#41;
        &#123;
          this.contextRoot = contextRoot;
        &#125;
      
      
      
        //
        // METHODS FROM INTERFACE URLComposer
        //
      
        public URL composeURL&#40;final String relativeURL&#41; throws MalformedURLException
        &#123;
          return new URL&#40;getContextComposer&#40;&#41;.composeURL&#40;getContextRoot&#40;&#41;&#41;, relativeURL&#41;;
        &#125;
      
        public String composeURLAsString&#40;String relativeURL&#41; throws MalformedURLException
        &#123;
          return composeURL&#40;relativeURL&#41;.toString&#40;&#41;;
        &#125;
      &#125;
      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

      Comment


      • #4
        Thanks, Andy this is great stuff. I am assuming that you do get some param (servername, port) passed to your richclient from either your dynamic servlets if you have to luanch the jnlp or some other way if it not webstart based?

        Amad

        Comment


        • #5
          Our app is WebStart based. You could programatically infer the base URL from WebStart APIs, which would be the easiest approach (BasicService.getCodeBase() for example). Our setup is a little more complex, so we dynamically generate the jnlp file on the server side and pass the base URL as a parameter to the app.

          - Andy

          Comment


          • #6
            I am using JnlpDownloadServlet, so I can pass the base URL as a paramter to app as well.

            Amad

            Comment

            Working...
            X