14 Replies Latest reply on Mar 15, 2011 4:03 AM by cbensemann

    Best practice for storing configuration properties

    swenvogel

      Hi,


      this question is not directly releated to seam but maybe interesting for others.
      I haven an complex seam based application with a lot of configurable properties.


      My first solution was to store all this properties with a Property class in the database:



      @Entity
      @Table(name = "properties", uniqueConstraints = { @UniqueConstraint(columnNames = "name") })
      public class Property extends BaseEntity {
           private static final long serialVersionUID = 1L;
      
           private String name;
           private String value;
           ...
      }
      



      The names of all properties are stored in a second class


      @Name("propertyNames")
      @Scope(ScopeType.APPLICATION)
      public class PropertyNames {
      
           /**
            * The common system email address
            */
           public static String COMMON_SYSTEMEMAIL_ADDRESS = "common.email.address";
      
           ...
      }
      




      The drawbacks of this approch are:



      • values must be converted to String and vice versa

      • the contexts of related properties are lost

      • because all values are Strings a wrapper must be written that supports getter and setter methods for different types (integer, boolean, etc.) for use in jsf input fields of different types.




      So, the second approach im thinking about is to store related configuration
      values in simple classes for example:


      public class LoginConfig implements Serializable {
           private static final long serialVersionUID = 1L;
      
           public static String CONFIG_NAME = "ca.login";
      
           private boolean firstLogin;
           private boolean accountEnabled;
           private String  loginText;
      
           ...
      }
      



      and store this classes throug a ObjectConfigurationClass in database:


      @Entity
      @Table(name = "configurations")
      public abstract class Configuration extends BaseEntity {
           private static final long serialVersionUID = 1L;
      
           private String name;
           private Object value = null;
           ...
      }
      



      This approach works fine and is easy to use with EL expressions. Also more compley values
      like ArrayList or HashMap are no problem.


      The question is, are there are any other or better ways for configuration handling?
      I want's to ask this question before i refactor the hole application :-)!

        • 1. Re: Best practice for storing configuration properties
          lvdberg

          Hi,


          I basically used your solution nr 1., but I added a type-field to the Entity (an enum representing the type of parameter) and additional utility methods which converts from a to the required type. So as an example the Attribute class and its  method for retrieving a List with Strings:






          
          public abstract class Attribute  {
          
               /** Timestamp when this Attribute was created in the Database */
               @Temporal(TemporalType.TIMESTAMP)
               private Date registrationTimestamp = new Date();
          
               /** Contains the key of the attribute. 
                * All keys are Strings.
                * For more more complex attributes extends this class and add the additional features.
                * 
                */
               @Column(name="aKey", length=100)
               private String key;
               
               /** Contains the value of the attribute. 
                * All values are string based "normal" types as defined by the attribute type.
                * More complex types sub-class Attribute and contain their own value/validation rules.
                * 
                */
               @Length(max=100)
               @Column(name="aValue", length=100)
               private String value;
               
               @Column(name="aType", length=3)
               private String type;
          
          ....
          
          }
          
          
          
               /** Returns the value of this attribute as a valid String.
                * cannot return an invalid value, only an empty String
                * @return Byte
                */
               public List<String> readStringList(){
                    List<String> response;
                    if (type.equalsIgnoreCase(AttributeType.SAR.name()) && value!= null && !value.isEmpty()){
                         StringTokenizer st = new StringTokenizer(value, ";");     
                         response = new ArrayList<String>();
                         while (st.hasMoreTokens()){
                              response.add(st.nextToken().trim());
                         }
                    } else response = Collections.emptyList(); // Empty string array
                    return response;
               }
               
          



          With this solution I restrict the max length of the value to 100, but you can replace it with a Lob if needed.


          Leo

          • 2. Re: Best practice for storing configuration properties
            lvdberg

            Hi,


            just saw problems with cutting and pasting, sorry for the errors in the comment part :-(

            • 3. Re: Best practice for storing configuration properties
              swenvogel

              Hi,


              yes i have the same solution (without the type field), in my Property class
              i have several methods like:


              @Transient
              public void setFloatValue(Float value) {
                  this.value = value != null ? value.toString() : null;
              }
              
              @Transient
              public Float getFloatValue() {
                  return value != null ? Float.parseFloat(value): null;
              }
              



              Additionally i have a class PropertyAction to access the properties in the UI.


              ...
              <h:inputText
                 value="#{applicationProperties.getProperty('bookingExport.hour').integerValue"}
              ...
              



              The first ugly point is that i hardcode the property name in the xhtml code.
              Sure i can write some kind of property reader class like:


              @Name("propertyReader")
              public class PropertyReader {
              
                  public int getBookingExportHour() {
                      value="#{applicationProperties.getProperty(
                         PropertyNames.BOOKINGEXPORT_HOUR).getIntegerValue()"}
                  }
              
                  public void setBookingExportHour(int hour) {
                      value="#{applicationProperties.getProperty(
                         PropertyNames.BOOKINGEXPORT_HOUR).setIntegerValue(hour)"}
                  }
              }
              



              But that is so much boilerplate code to decouple the UI. And i have more than 200 Properties in my application! With the second solution this would be not necessary.


              It would be possible to acces typed properties like


              <h:inputText value="#{configManager.bookingExportConfig.hour}" />
              



              or simpler with a factory method in the ConfigManager:


              <h:inputText value="#{bookingExportConfig.hour}" />
              





              • 4. Re: Best practice for storing configuration properties
                cbensemann

                The system I've created in my current project is working quite well for me. Basically what I have done is write an interceptor which injects properties into seam components in a very similar way to @In


                So in my seam component I would have


                @Name("mySeamComponent")
                @ManagedProperties
                public class MySeamComponent {
                
                    @Property
                    private Long thresholdLevel1;
                
                    @Property(name="thresholdLevel2")
                    private Long anotherThreshold;
                }



                The first property is looked up based on the field name. The second one is based on the name parameter of the annotation.


                What the interceptor then does is delegate to a session scoped component (propertyManager) which looks up the database to see if there is a property with that name and sets it on that field.


                I then went a step further and added user specific properties so the property manager would look up user specific values then fall back to default values if that didn't exist. This lets me specify global defaults while letting users override them to suit their needs.


                If injection isn't quite what I need I am also able to look up properties directly using the property manager mentioned above.


                #{propertyManager.get("thresholdLevel1")}



                Currently it only works with simple types (Long, Integer, String etc etc) as I've never had a need for more complex types but I guess you could extend it to do so if needed.



                Craig


                • 5. Re: Best practice for storing configuration properties
                  markwigmans

                  Interesting idea about the annotation. Could you share your source code with us?

                  • 6. Re: Best practice for storing configuration properties
                    kragoth

                    Craig Bensemann wrote on Nov 05, 2010 21:08:


                    The system I've created in my current project is working quite well for me. Basically what I have done is write an interceptor which injects properties into seam components in a very similar way to @In

                    So in my seam component I would have

                    @Name("mySeamComponent")
                    @ManagedProperties
                    public class MySeamComponent {
                    
                        @Property
                        private Long thresholdLevel1;
                    
                        @Property(name="thresholdLevel2")
                        private Long anotherThreshold;
                    }



                    The first property is looked up based on the field name. The second one is based on the name parameter of the annotation.

                    What the interceptor then does is delegate to a session scoped component (propertyManager) which looks up the database to see if there is a property with that name and sets it on that field.

                    I then went a step further and added user specific properties so the property manager would look up user specific values then fall back to default values if that didn't exist. This lets me specify global defaults while letting users override them to suit their needs.

                    If injection isn't quite what I need I am also able to look up properties directly using the property manager mentioned above.

                    #{propertyManager.get("thresholdLevel1")}



                    Currently it only works with simple types (Long, Integer, String etc etc) as I've never had a need for more complex types but I guess you could extend it to do so if needed.


                    Craig




                    Nice one :) I may use this some day.


                    Mark, interceptors are incredibly easy to write. Just check the Seam doco for an example. :)


                    Then it's just a matter of using standard class api to get the vars with the @Property annotation and using either the name of the var or the value of the name attribute of the annotation as your param to your PropertyManager service to set the value.

                    • 7. Re: Best practice for storing configuration properties
                      swenvogel

                      Really an interesting solution. How do you access your properties from the UI/XHTML code?

                      • 8. Re: Best practice for storing configuration properties
                        cbensemann

                        Hi,


                        Sorry for the delay. New seam project went into production this week so have been busy with that.


                        I'm happy to share the code for this. As Tim wrote interceptors aren't all that difficult in seam but can be quite handy.


                        So here goes. First you need an annotation which you will mark your fields in your components (the same as @In


                        @Target( { FIELD })
                        @Documented
                        @Retention(RUNTIME)
                        @Inherited
                        public @interface Property {
                            String name() default "";
                        }
                        



                        Next you need the interceptor class which will look for the above annotation and try to inject a property (the actual code for looking up properties is delegated off as we will see below)


                        @SuppressWarnings("serial")
                        @Interceptor(stateless = true)
                        public class PropertyInterceptor extends AbstractInterceptor {
                        
                            /*
                             * (non-Javadoc)
                             * 
                             * @see org.jboss.seam.intercept.OptimizedInterceptor#aroundInvoke(org.jboss. seam.intercept.InvocationContext)
                             */
                            @Override
                            @AroundInvoke
                            public Object aroundInvoke(final InvocationContext invocationContext) throws Exception {
                                for (final Field field : getPropertyFields()) {
                                    final Object target = invocationContext.getTarget();
                                    if (field.get(target) == null) {
                                        field.setAccessible(true);
                                        field.set(target, getPropertyManager().get(getUserPropertyName(field), field.getType()));
                                    }
                                }
                        
                                return invocationContext.proceed();
                            }
                        
                            /*
                             * (non-Javadoc)
                             * 
                             * @see org.jboss.seam.intercept.OptimizedInterceptor#isInterceptorEnabled()
                             */
                            @Override
                            public boolean isInterceptorEnabled() {
                                return !getPropertyFields().isEmpty();
                            }
                        
                            private String getUserPropertyName(final Field field) {
                                final Property p = field.getAnnotation(Property.class);
                        
                                if (p.name() == null || p.name().isEmpty()) {
                                    return field.getName();
                                }
                                return p.name();
                            }
                        
                            private List<Field> getPropertyFields() {
                                Class<?> clazz = getComponent().getBeanClass();
                                final List<Field> fields = new LinkedList<Field>();
                                for (; clazz != Object.class; clazz = clazz.getSuperclass()) {
                                    for (final Field field : clazz.getDeclaredFields()) {
                                        if (!field.isAccessible()) {
                                            field.setAccessible(true);
                                        }
                                        if (field.isAnnotationPresent(Property.class)) {
                                            fields.add(field);
                                        }
                                    }
                                }
                        
                                return fields;
                            }
                        
                            private PropertyManager getPropertyManager() {
                                final PropertyManager instance = (PropertyManager) Component.getInstance("propertyManager", true);
                                if (instance == null) {
                                    throw new IllegalStateException("Property Manager not installed. Please configure in components.xml");
                                }
                                return instance;
                            }
                        }
                        



                        Now we have a couple of options. We can add our interceptor as a default interceptor by adding it to the default list of interceptors in your components.xml or by manually adding it to the classes you want intercepted with an annotation. I picked the second option so here is the annotation


                        @Target(ElementType.TYPE)
                        @Retention(RetentionPolicy.RUNTIME)
                        @Documented
                        @Inherited
                        @Interceptors(PropertyInterceptor.class)
                        public @interface ManagedProperties {
                        }
                        



                        Now that everything is set up we still need the property manager class which is going to get the properties from the DB (or other source). This is the class I'm not fully happy with. I wanted to make a reusable way to provide both user specific properties and then defaults if no user specific one exisits. I'll just post it as it is cause this post is getting quite long. Happy to discuss it further and take any improvements you might have :)


                        /**
                         * Copyright Software Factory - 2010
                         */
                        package nz.co.softwarefactory.xc.configuration;
                        
                        import java.io.Serializable;
                        import java.text.ParseException;
                        import java.text.SimpleDateFormat;
                        import java.util.Date;
                        import java.util.HashMap;
                        import java.util.Map;
                        
                        import javax.persistence.EntityManager;
                        
                        import org.jboss.seam.Component;
                        import org.jboss.seam.ScopeType;
                        import org.jboss.seam.annotations.Create;
                        import org.jboss.seam.annotations.Install;
                        import org.jboss.seam.annotations.Logger;
                        import org.jboss.seam.annotations.Name;
                        import org.jboss.seam.annotations.Scope;
                        import org.jboss.seam.log.Log;
                        import org.jboss.seam.util.AnnotatedBeanProperty;
                        
                        /**
                         * Class that is able to look up the user config in a semi-generic way and if no configuration option is present will
                         * read a value from the default properties table. Not entirely happy with the way the userconfig is retrieved.
                         *
                         * @author craig
                         */
                        @SuppressWarnings("serial")
                        @Name("propertyManager")
                        @Install(false)
                        @Scope(ScopeType.SESSION)
                        public class PropertyManager implements Serializable {
                        
                            @Logger
                            private Log log;
                        
                            private AnnotatedBeanProperty<UserConfiguration> userConfiguration;
                        
                            /**
                             * Class which contains the @UserConfiguration annotation.
                             */
                            private Class<?> configurationWrapperClass;
                        
                            /**
                             * DB ID for the wrapper class so that a current copy can be looked up.
                             */
                            private Object configurationWrapperId;
                        
                            @Create
                            public void init() {
                                if (configurationWrapperClass == null) {
                                    log
                                            .warn("No configuration wrapper class has been defined. PropertyManager may not be configured correctly!");
                                }
                                else {
                                    userConfiguration = new AnnotatedBeanProperty<UserConfiguration>(configurationWrapperClass,
                                            UserConfiguration.class);
                                }
                            }
                        
                            @SuppressWarnings("unchecked")
                            Map<String, String> getUserConfiguration() {
                                final Object configurationWrapper = getConfigurationWrapper();
                                if (configurationWrapper == null) {
                                    return new HashMap<String, String>();
                                }
                                return (Map<String, String>) userConfiguration.getValue(configurationWrapper);
                            }
                        
                            /**
                             * @return
                             */
                            private Object getConfigurationWrapper() {
                                if (configurationWrapperId == null) {
                                    return null;
                                }
                                return getEntityManager().find(configurationWrapperClass, configurationWrapperId);
                            }
                        
                            public Integer getAsInt(final String name) {
                                return Integer.parseInt(get(name));
                            }
                        
                            public String get(final String name) {
                                return getUserPropertyOrDefault(name);
                            }
                        
                            @SuppressWarnings("unchecked")
                            public <T> T get(final String name, final Class<T> type) {
                                final String property = get(name);
                        
                                final Object convertedProperty;
                        
                                // TODO this is a bit yuck. surely there must be a way to use overloaded methods to make this work.
                                if (String.class.isAssignableFrom(type)) {
                                    convertedProperty = property;
                                }
                                else if (Integer.class.isAssignableFrom(type)) {
                                    convertedProperty = convertIntegerProperty(property);
                                }
                                else if (Long.class.isAssignableFrom(type)) {
                                    convertedProperty = convertLongProperty(property);
                                }
                                else if (Double.class.isAssignableFrom(type)) {
                                    convertedProperty = convertDoubleProperty(property);
                                }
                                else if (Date.class.isAssignableFrom(type)) {
                                    convertedProperty = convertDateProperty(property);
                                }
                                else {
                                    convertedProperty = convertObjectProperty(property, type);
                                }
                        
                                return (T) convertedProperty;
                            }
                        
                            public Integer convertIntegerProperty(final String property) {
                                return Integer.parseInt(property);
                            }
                        
                            public Double convertDoubleProperty(final String property) {
                                return Double.parseDouble(property);
                            }
                        
                            public Long convertLongProperty(final String property) {
                                return Long.parseLong(property);
                            }
                        
                            public Date convertDateProperty(final String property) {
                                try {
                                    return new SimpleDateFormat("dd/MM/yyyy").parse(property);
                                }
                                catch (final ParseException e) {
                                    // TODO Auto-generated catch block
                                    e.printStackTrace();
                                    throw new RuntimeException(e);
                                }
                            }
                        
                            public Object convertObjectProperty(final String property, final Class<?> type) {
                                throw new UnsupportedOperationException("Unsupported type: " + type.getClass());
                            }
                        
                            /**
                             * Attempts to get a user specific config value for the passed in <code>name</code>. If no value is found will
                             * attempt to get a system default from the defaults table.
                             *
                             * @param name
                             * @return
                             */
                            String getUserPropertyOrDefault(final String name) {
                                final String userValue = getUserConfiguration().get(name);
                                if (userValue == null || userValue.isEmpty()) {
                                    return getDefault(name);
                                }
                                return userValue;
                            }
                        
                            /**
                             * @param name
                             * @return
                             * @see DefaultProperty
                             */
                            private String getDefault(final String name) {
                                try {
                                    return ((DefaultProperty) getEntityManager().createQuery("FROM DefaultProperty dp WHERE dp.name = :name")
                                            .setParameter("name", name).getSingleResult()).getValue();
                                }
                                catch (final Exception e) {
                                    // TODO log this???
                                    return null;
                                }
                            }
                        
                            private EntityManager getEntityManager() {
                                return (EntityManager) Component.getInstance("entityManager", true);
                            }
                        
                            public void setConfigurationWrapperClass(final Class<?> configurationWrapperClass) {
                                this.configurationWrapperClass = configurationWrapperClass;
                            }
                        
                            public void setConfigurationWrapperId(final Object configurationWrapperId) {
                                this.configurationWrapperId = configurationWrapperId;
                            }
                        }
                        



                        the property manager is not installed by default so you would need to specify it in your components.xml

                        • 9. Re: Best practice for storing configuration properties
                          cbensemann

                          To answer your question Swen. To use this system in your xhtml you have a couple of options.


                          You could access your property through a backing component bean


                          @Property
                          @Out
                          private String myproperty;
                          



                          or with getters for myproperty


                          You could wrap the property up something like this.


                          @SuppressWarnings("serial")
                          @ManagedProperties
                          @Name("xc.motd")
                          @Scope(ScopeType.SESSION)
                          public class MessageOfTheDay implements Serializable {
                          
                              @Property
                              private String motd;
                          
                              @Unwrap
                              public String findMotd() {
                                  return motd;
                              }
                          }
                          



                          or because the logic of actually looking up the property is delegated off to a seam component called propertyManager you can look up your property directly using EL


                          #{propertyManager.get("myproperty"}}
                          




                          hope this helps
                          Craig

                          • 10. Re: Best practice for storing configuration properties
                            antibrumm.mfrey0.bluewin.ch

                            Hi
                            We use also the database to store our system / user preferences. However we do not convert the values to varchar. Instead we use an abstractpreferencevalue class with disriminated subclasses mapped on a single table. Like this you can avoid the casts and be more save on the stored values.

                            • 11. Re: Best practice for storing configuration properties
                              antibrumm.mfrey0.bluewin.ch

                              Here's the manager class we use to propagate the preferences


                              The NotificationPreferences is an extended class of AbstractPreferences:


                              /**
                               * The Class NotificationPreferencesHome.
                               */
                              @Name("notificationPreferencesHome")
                              public class NotificationPreferencesHome extends EntityHome<NotificationPreferences> {
                              
                                   /** The Constant serialVersionUID. */
                                   private static final long serialVersionUID = 1L;
                              
                                   /**
                                    * Instantiates a new notification preferences home.
                                    */
                                   public NotificationPreferencesHome() {
                                        super();
                                        setId(NotificationPreferences.ID);
                                   }
                              
                                   /**
                                    * Gets the edits the instance.
                                    * 
                                    * @return the edits the instance
                                    */
                                   @Factory(value = "editNotificationPreferences", scope = ScopeType.CONVERSATION, autoCreate = true)
                                   public NotificationPreferences getEditInstance() {
                                        return getInstance();
                                   }
                              
                                   /*
                                    * (non-Javadoc)
                                    * @see org.jboss.seam.framework.Home#getInstance()
                                    */
                                   @Override
                                   @Factory(value = "notificationPreferences", scope = ScopeType.SESSION, autoCreate = true)
                                   public NotificationPreferences getInstance() {
                                        return super.getInstance();
                                   }
                              
                                   /*
                                    * (non-Javadoc)
                                    * @see org.jboss.seam.framework.Home#handleNotFound()
                                    */
                                   @Override
                                   protected NotificationPreferences handleNotFound() {
                                        return new NotificationPreferences();
                                   }
                              
                                   /**
                                    * Save.
                                    * 
                                    * @return the string
                                    */
                                   @Transactional
                                   public String save() {
                                        if (isManaged()) {
                                             return update();
                                        } else {
                                             return persist();
                                        }
                                   }
                              }
                              



                              This is an example of preferences the admin can change at any time from inside the application. These preferences are SESSION scoped because we expect that the values dont change daily and like this we avoid unneccessary db access.


                              • 12. Re: Best practice for storing configuration properties
                                lvdberg

                                Hi,


                                Thanks for the advices in this thread. I would definitely vote for such a standard component in Seam (3) !!


                                Leo

                                • 13. Re: Best practice for storing configuration properties
                                  johncarl81

                                  A note on this approach... Ive adapted this approach to use .property file attributes just like you injected properties out of a database source.  Very handy!


                                  John

                                  • 14. Re: Best practice for storing configuration properties
                                    cbensemann

                                    Funny you should mention that John. I have just been doing a similar thing. My goal was to allow the use of multiple data sources (eg Database, properties files, System properties etc) to define the properties and to specify them in an order which allows for property overriding. For example you could have user specific properties which would be used but if no user specific property was found a default was used.


                                    In addition to that I wanted to be able to access properties via EL. I'm not going to describe it all again here but I have written it up on my seam tips page. As always it probably needs proof reading and more descriptions added in places but feel free to have a look and comment.


                                    https://sites.google.com/a/thesoftfact.com/seam-tips/configuration/ This page describes the entire way we do configuration in our apps. The part most related to this post is https://sites.google.com/a/thesoftfact.com/seam-tips/configuration/properties-manager