Skip to content

Bean properties

ljacqu edited this page Jan 18, 2020 · 9 revisions

ConfigMe allows you to use properties of a custom JavaBean class type. This is useful when you have more complicated structures that belong together as a whole, which you'd like to map onto one class.

Introduction

Let's look at an example. Given the following JavaBean class:

public class Country {
  private String name;
  private int population;

  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }

  public int getPopulation() {
    return population;
  }
  public void setPopulation(int population) {
    this.population = population;
  }
}

The following yml file:

country:
  name: 'Denmark'
  population: 5614000
neighbor:
  name: 'Sweden'
  population: 9593000

and a SettingsHolder class:

public class Countries implements SettingsHolder {
  public static final Property<Country> COUNTRY =
    newBeanProperty(Country.class, "country", new Country());

  public static final Property<Country> NEIGHBOR =
    newBeanProperty(Country.class, "neighbor", new Country());
}

If we set up a SettingsManager with the above classes and yml file, we'll get the following result:

// Denmark
String countryName = settingsManager.getProperty(Countries.COUNTRY).getName();

// Country object for Sweden
Country neighbor = settingsManager.getProperty(Countries.NEIGHBOR);
int neighborPop = neighbor.getPopulation(); // 9593000

JavaBeans

JavaBeans are simple Java classes with getters and setters. As you can see in the Country example, each property is private and has a getter and setter method with standard naming conventions. ConfigMe picks up on properties which have both a getter and a setter and will look in the YML file for those properties to build a bean object.

Error handling

As with all other properties, bean properties never resolve to null, i.e. when you do settingsManager.getProperty(property) it is guaranteed to never return null. If the contents in the YML file cannot be used to create a bean (e.g. because a field is missing), the default value of the property is used.

You can define a default value in your bean by assigning it directly to a field. For example, if the name of a country should be optional, we could assign it to an empty string at the beginning:

public class Country {
  private String name = "";
  private int population;

  // getters and setters
}

Now if your YML has something like:

country:
  population: 1234

The Countries.COUNTRY property will resolve to a Country object that has an empty name and its population set to 1234. We could also omit the population: since the field is a primitive it will automatically be assigned a value of 0 on initialization.

ConfigMe will never return a bean in which a field is null. If it does not succeed to set a field's value, the mapping is considered to have failed and the default object is used. It is therefore important to initialize fields as shown with the country name above if you don't want the absence of one field to make ConfigMe ignore all fields and use the default bean object provided on initialization.

Using beans within beans

The JavaBean classes can use other JavaBean classes, recursively. As an example, if we want to add the country's president to our configuration, we can create a President class:

public class President {
  private String name;
  private int sinceYear;

  // getters and setters
}

We can integrate this new class into the Country class:

public class Country {
  private String name;
  private int population;
  private President leader;

  // getters and setters
}

Update your YML:

country:
  name: 'Latvia'
  population: 1957000
  leader:
    name: 'Raimonds Vējonis'
    sinceYear: 2015

This gives you the opportunity to look up the country's president in your code:

Country country = settingsManager.getProperty(Countries.COUNTRY);
System.out.println(country.getLeader().getName()); // Raimonds Vējonis

Collections

You can use collections (such as List and Set) in your beans. For example, we can change the Country class to contain a list of neighbors:

public class Country {
  private String name;
  private int population;
  private List<String> neighbors;

  // getters and setters for all three properties
}

Given the YML:

country:
  name: Denmark
  population: 5614000
  neighbors:
  - Sweden
  - Norway
  - Germany

This will create a Country object with getNeighbors() containing a list of "Sweden", "Norway", "Germany". Again, if this property should be optional, you can set private List<String> neighbors = new ArrayList<>(); in the bean class.

Restrictions

  • The type argument of collections must always have a specific type on the fields of JavaBean classes, i.e. you cannot have a field List<?> neighbors or List neighbors. It must have a specific type, such as List<Country> neighbors.
  • Fields with a type more specific than List or Set are not supported. For instance, Iterable<String> neighbors, Collection<String>, Set<String> are fine, but specific types such as TreeSet<String> are not.
  • You can declare collections of other bean types, e.g. you can have a List<Country> field. If an entry in the YML cannot be mapped to a Country, the entire entry is skipped.

Maps

You can also use Map properties in your beans. Only maps with String keys are supported, e.g. Map<String, Integer> or Map<String, Country> are fine, but something like Map<Integer, Country> is not supported. Maps are useful when you want to support arbitrary keys in a section, e.g.:

country:
  name: Denmark
  population: 123
  neighbors:
    sweden:
      name: Sweden
    norway:
      name: Norway

Under neighbors you want to support any key that is defined. This is typically the most user-friendly way when you want to have a dynamic section of a bean type. For the above YML, your JavaBean has to be as follows:

public class Country {
  private String name;
  private int population;
  private Map<String, Country> neighbors = new HashMap<>();

 // getters and setters for all three properties
}

Country country = settingsManager.getProperty(Countries.COUNTRY);
country.getNeighbors(); // Returns map with "sweden" and "norway"

If certain keys have a special meaning in your configuration, you should favor individual properties in your class rather than loading everything into a map. Maps are convenient when you want to allow arbitrary keys which you don't know beforehand, as in this example.

Saving and loading

You may observe some non-intuitive behavior when working with bean properties.

The value you retrieve from SettingsManager#getProperty(BeanProperty) will always be the same object as long as you don't reload. However, if you change the bean property, it is recommended to explicitly call SettingsManager#setProperty to make your intention clear and to future-proof it.

As with other properties, remember that the settings manager might provide the default value to you! In such cases, you probably don't want to change the actual default value but make a copy of it first.

Real-life example

CommandSettingsHolder, which declares a bean property of type CommandConfig


Navigation

« Migration service Bean properties Custom property types »