Loading definitions from a database

A typical request from Tiles users was to be able to load Tiles definitions from a database. With Tiles 2.1, it is a pretty easy task.

The source of the example is available in the source distribution of Tiles, under the "tiles-test" module.

Database design

The first step is to design the database schema. Here we will show a small example that is far from perfect, anyway it will be enough for our example. The following is the picture of the database schema we will use.

Database schema that will be used for the example.

Create a Definition DAO

Essentially, the main programming task is the development of a definition DAO. This interface is designed to provide an easy customization of definitions retrieval.

The method that you should implement is only getDefinition. You don't have to implement getDefinitions too completely (a simple throw UnsupportedOperationException will be enough) since it won't be called at all.

getDefinition

This is the source of getDefinition in our example: the JDBC DAO support of Spring is used to have a cleaner code.

/** {@inheritDoc} */
@SuppressWarnings("unchecked")
public Definition getDefinition(String name, Locale locale) {
    List<Map<String, Object>> customizations = null;
    Long customizationId = null, parentCustomizationId = null;
    do {
        customizations = getJdbcTemplate().queryForList(
                SELECT_CUSTOMIZATION_BY_NAME_SQL,
                new Object[] { locale.toString() });
        if (!customizations.isEmpty()) {
            Map<String, Object> customization = customizations.get(0);
            customizationId = ((Number) customization.get("ID")).longValue();
            parentCustomizationId = numberToLong((Number) customization.get("PARENT_ID"));
        } else {
            locale = LocaleUtil.getParentLocale(locale);
        }
    } while (customizations.isEmpty());

    return getDefinition(name, customizationId, parentCustomizationId,
            locale);
}

In other words:

  1. the current customization (in this case, the client's locale) is identified;
  2. it is tried to retrieve the locale from the DB: if it is not found, it tries with the parent locale (the parent locale of "en_US" is "en") until one is found, or the default (no locale) is used;
  3. the definition for the supported minimum-parent-locale is retrieved and passed to the caller.

Retrieval of the definition from the DB

At this point the definition must be retrieved from the DB.

@SuppressWarnings("unchecked")
protected DbDefinition getDefinition(String name, Long baseCustomizationId,
        Long baseParentCustomizationId, Locale locale) {
    DbDefinition definition = null;
    Long customizationId = baseCustomizationId;
    Long parentCustomizationId = baseParentCustomizationId;
    List<DbDefinition> definitions = null;
    boolean finished = false;
    do {
        definitions = getJdbcTemplate()
                .query(SELECT_DEFINITION_SQL,
                        new Object[] { name, customizationId },
                        definitionRowMapper);
        if (definitions.isEmpty()) {
            if (parentCustomizationId != null) {
                Map<String, Object> customization = getJdbcTemplate().queryForMap(
                        SELECT_CUSTOMIZATION_BY_ID_SQL,
                        new Object[] { parentCustomizationId });
                customizationId = ((Number) customization.get("ID")).longValue();
                parentCustomizationId = numberToLong((Number) customization.get("PARENT_ID"));
            } else {
                finished = true;
            }
        } else {
            definition = definitions.get(0);
            finished = true;
        }
    } while (!finished);

    if (definition != null) {
        AttributeRowMapper attributeRowMapper = new AttributeRowMapper(definition);
        getJdbcTemplate().query(SELECT_ATTRIBUTES_SQL,
                new Object[] { definition.getId() }, attributeRowMapper);
    }
    return definition;
}

The steps that are followed are:

  1. search for a definition that is usable with the customization id (id of the locale in the DB) that is suggested by the caller;
  2. if the definition has not been found, the parent customization id is used and the operation at point 1 is done, until a definition is found;
  3. if the definition has been found, the attributes are loaded from the DB.

Notice that the definition's inheritance is not resolved because it will be done by the implementation of DefinitionsFactory that will call the definitions DAO multiple times to resolve inheritance.

The DbDefinition is a simple extension of Definition with the addition of an id.

Configuration

To use the definitions DAO we need to configure Tiles to use an alternate DefinitionsFactory that is LocaleDefinitionsFactory. This definitions factory resolves definitions one by one, by retrieving all extended definitions through calls to the definition DAO.

Though it may seem slow, it is memory-efficient and it is effective when the number of definitions is high.

Here will be using pure Java configuration. A new class extending BasicTilesContainerFactory must be created. It will be called TestDbTilesContainerFactory. This is the source:

public class TestDbTilesContainerFactory extends BasicTilesContainerFactory {

    /** {@inheritDoc} */
    @Override
    protected DefinitionDAO<Locale> createLocaleDefinitionDao(Object context,
            TilesApplicationContext applicationContext,
            TilesRequestContextFactory contextFactory, LocaleResolver resolver) {
        LocaleDbDefinitionDAO definitionDao = new LocaleDbDefinitionDAO();
        definitionDao.setDataSource((DataSource) applicationContext
                .getApplicationScope().get("dataSource"));
        return definitionDao;
    }

    /** {@inheritDoc} */
    @Override
    protected LocaleDefinitionsFactory instantiateDefinitionsFactory(
            Object context, TilesApplicationContext applicationContext,
            TilesRequestContextFactory contextFactory, LocaleResolver resolver) {
        return new LocaleDefinitionsFactory();
   }
}

Create a Tiles listener this way:

public class TestDbTilesListener extends AbstractTilesListener {

    @Override
    protected TilesInitializer createTilesInitializer() {
        return new TestDbTilesInitializer();
    }

    private static class TestDbTilesInitializer extends AbstractTilesInitializer {

        @Override
        protected AbstractTilesContainerFactory createContainerFactory(
                TilesApplicationContext context) {
            return new TestDbTilesContainerFactory();
        }
    }
}

In web.xml add this piece of configuration:

<listener>
  <listener-class>org.apache.tiles.test.listener.TestDbTilesListener</listener-class>
</listener>

And you're done!