DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Taming Billions of Rows: How Metadata and SQL Can Replace Your ETL Pipeline
  • Reading Table Metadata With Flight SQL
  • SeaweedFS vs. JuiceFS Design and Features
  • How To Approach Java, Databases, and SQL [Video]

Trending

  • DZone's Article Submission Guidelines
  • How to Submit a Post to DZone
  • Rethinking Java CRUDs With Event Sourcing and CQRS Patterns
  • Implementing Secure API Gateways for Microservices Architecture
  1. DZone
  2. Data Engineering
  3. Databases
  4. How to Build a Metadata-Driven UI

How to Build a Metadata-Driven UI

A metadata-driven UI provides project teams with element alignment by invoking a single endpoint for data. Here's how to build your own metadata-driven UI.

By 
Sergei Visotsky user avatar
Sergei Visotsky
·
Oct. 26, 21 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
10.6K Views

Join the DZone community and get the full member experience.

Join For Free

A metadata-driven UI approach is especially useful in project teams with a high back-end or DBA competence rather than UI. In general, it provides element alignment by invocation of a single endpoint that provides all data required such as cardinality, language, font size, and the font itself.

The library itself aims to provide a configurable metadata engine and a set of endpoints. At the same time, UI should be written from scratch, taking into account corresponding use case specifics to be able to properly handle metadata and construct itself based on it.

Getting Started

To get started with the usage of a metadata provider, just add the following maven dependency to the main application:

XML
 
<dependency>
    <groupId>io.github.sergeivisotsky.metadata</groupId>
    <artifactId>metadata-selector</artifactId>
</dependency>


And the following dependency to a deployment application:

XML
 
<dependency>
    <groupId>io.github.sergeivisotsky.metadata</groupId>
    <artifactId>metadata-deploy</artifactId>
</dependency>


To have a compatible version of both dependencies, it is also highly recommended to add a library starter to your dependencyManagement section of parent POM:

XML
 
<dependency>
    <groupId>io.github.sergeivisotsky.metadata</groupId>
    <artifactId>metadata-provider-bom</artifactId>
    <version>0.0.7</version>
    <scope>import</scope>
    <type>pom</type>
</dependency>


It will be loaded from Maven central.

Extension

Let's imagine we have the following preconfigured form metadata provider, which was crafted from the following preconfigured template.

Java
 
@Component
public class FormMetadataMapper implements MetadataMapper<FormMetadata> {

    @Override
    public String getSql() {
        return "SELECT fm.id,\n" +
                "       fm.form_name,\n" +
                "       fm.cardinality,\n" +
                "       fm.language,\n" +
                "       fm.offset,\n" +
                "       fm.padding,\n" +
                "       fm.font,\n" +
                "       fm.font_size,\n" +
                "       fm.description,\n" +
                "       fm.facet,\n" +
                "       vf.enabled_by_default,\n" +
                "       vf.ui_control\n" +
                "FROM form_metadata fm\n" +
                "         LEFT JOIN view_field vf on fm.id = vf.form_metadata_id\n" +
                "WHERE fm.form_name = :formName\n" +
                "  AND fm.language = :lang";
    }

    @Override
    public ExtendedFormMetadata map(ResultSet rs) {
        try 
            ExtendedFormMetadata metadata = new ExtendedFormMetadata();
            metadata.setFormName(rs.getString("form_name"));
            metadata.setCardinality(rs.getString("cardinality"));
            metadata.setLang(Language.valueOf(rs.getString("language")
                    .toUpperCase(Locale.ROOT)));
            metadata.setOffset(rs.getInt("offset"));
            metadata.setPadding(rs.getInt("padding"));
            metadata.setFont(rs.getString("font"));
            metadata.setFontSize(rs.getInt("font_size"));
            metadata.setDescription(rs.getString("description"));
            ViewField viewField = new ViewField();
            viewField.setEnabledByDefault(rs.getInt("enabled_by_default"));
            viewField.setUiControl(rs.getString("ui_control"));
            metadata.setViewField(viewField);
            metadata.setFacet(rs.getString("facet"));
            return metadata;
        } catch (SQLException e) {
            throw new RuntimeException("Unable to get value from ResultSet for Mapper: {}" +
                    FormMetadataMapper.class.getSimpleName(), e);
        }
    }
}


From the first glance, this is more than enough. However, for a delivery project's specific needs it is necessary to add an additional structure that will represent some mysterious footer data.

This requires the following steps:

  1. Create a corresponding database table/new fields by means of adjusting deployment Liquibase scripts.
  2. Add a new structure in a preconfigured domain model like `ExtendedFormMetadata` or create a completely new one that will be a part of the form metadata.
  3. Adjust `FormMetadataMapper` or create a completely new mapper in case of the new requirements.

However, let's move to our example of a mysterious footer.

We have a requirement that:

  1. Web page footer should be generated from metadata.
  2. It should be bumped up in the response of the OOTBS metadata endpoint.

Step One

Create a new deployment Liquibase script.

In our case, it is just called db.changelog-12-09-2021.xml

XML
 
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">

    <changeSet id="1" author="svisockis">
        <createTable tableName="footer">
            <column name="id" type="java.sql.Types.BIGINT" autoIncrement="true">
                <constraints nullable="false" primaryKey="true"/>
            </column>
            <column name="resizable" type="java.sql.Types.BOOLEAN"/>
            <column name="displayable" type="java.sql.Types.BOOLEAN"/>
            <column name="defaultText" type="java.sql.Types.VARCHAR(150)"/>
            <column name="form_metadata_id" type="java.sql.Types.BIGINT"/>
        </createTable>
        <addForeignKeyConstraint baseTableName="footer" baseColumnNames="form_metadata_id"
                                 constraintName="footer_form_view_metadata_fk"
                                 referencedTableName="form_metadata"
                                 referencedColumnNames="id"/>
    </changeSet>
</databaseChangeLog>


Our footer metadata should hold information on whether the footer will be resizable and displayable, as well as default text that the user will see after the page is generated and a foreign key to the metadata base table.

Step Two

Create a corresponding POJO class.

Java
 
public class Footer {
    private Long id;
    private Boolean displayable;
    private Boolean resizable;
    private String defaultText;
  
    // Constructor, getter and setters omitted
}


Add a reference to parent POJO like this:

Java
 
public class ExtendedFormMetadata extends FormMetadata {
    private String facet;
    private Footer footer;

    // Constructor, getters and setters omitted
}


Step Three

Adjust a corresponding mapper — `FormMetadataMapper`in our case.

  1. SQL should be adjusted.
  2. Result set extraction should be adjusted.
Java
 
@Component
public class FormMetadataMapper implements MetadataMapper<FormMetadata> {

    @Override
    public String getSql() {
        return "SELECT fm.id,\n" +
                "       fm.form_name,\n" +
                "       fm.cardinality,\n" +
                "       fm.language,\n" +
                "       fm.offset,\n" +
                "       fm.padding,\n" +
                "       fm.font,\n" +
                "       fm.font_size,\n" +
                "       fm.description,\n" +
                "       fm.facet,\n" +
                "       vf.enabled_by_default,\n" +
                "       vf.ui_control,\n" +
                "       ft.displayable,\n" +         // new
                "       ft.resizable,\n" +           // new
                "       ft.default_Text\n" +         // new
                "FROM form_metadata fm\n" +
                "         LEFT JOIN view_field vf on fm.id = vf.form_metadata_id\n" +
                "         LEFT JOIN footer ft on fm.id = ft.form_metadata_id\n" +      // new
                "WHERE fm.form_name = :formName\n" +
                "  AND fm.language = :lang";
    }

    @Override
    public ExtendedFormMetadata map(ResultSet rs) {
        try {
            ExtendedFormMetadata metadata = new ExtendedFormMetadata();
            metadata.setFormName(rs.getString("form_name"));
            metadata.setCardinality(rs.getString("cardinality"));
            metadata.setLang(Language.valueOf(rs.getString("language")
                    .toUpperCase(Locale.ROOT)));
            metadata.setOffset(rs.getInt("offset"));
            metadata.setPadding(rs.getInt("padding"));
            metadata.setFont(rs.getString("font"));
            metadata.setFontSize(rs.getInt("font_size"));
            metadata.setDescription(rs.getString("description"));
            ViewField viewField = new ViewField();
            viewField.setEnabledByDefault(rs.getInt("enabled_by_default"));
            viewField.setUiControl(rs.getString("ui_control"));
            metadata.setViewField(viewField);
            metadata.setFacet(rs.getString("facet"));
            
            // --- New block ---
            Footer footer = new Footer();
            footer.setResizable(rs.getBoolean("resizable"));
            footer.setDisplayable(rs.getBoolean("displayable"));
            footer.setDefaultText(rs.getString("default_text"));
            metadata.setFooter(footer);
            // --- End new block ---
            
            return metadata;
        } catch (SQLException e) {
            throw new RuntimeException("Unable to get value from ResultSet for Mapper: {}" +
                    FormMetadataMapper.class.getSimpleName(), e);
        }
    }
}


Step Four

Run the deployer application to update the database schema and the application itself.

Result

In the result, you can see the following new section in the metadata endpoint.

JSON
 
}
// ...
   "footer": {
      "id": null,
      "displayable": true,
      "resizable": false,
      "defaultText": "This is some footer needed to fulfill our business requirements"
   }
 // ...
}


A source code of this demo can be found in the following repository.

Sample OOTB (Out-of-the-Box) Usage

The following page describes an OOTB (Out-of-the-Box) combo box metadata feature.

For a combo box style and values, metadata is used as well. As an example:

JSON
 
[
  {
    "id": 1,
    "codifier": "CD_001",
    "font": "Times New Roman",
    "fontSize": 12,
    "weight": 300,
    "height": 20,
    "displayable": true,
    "immutable": false,
    "comboContent": [
      {
        "key": "initial",
        "defaultValue": "Some initial value",
        "comboId": 1
      },
      {
        "key": "secondary",
        "defaultValue": "Some secondary value",
        "comboId": 1
      },
      {
        "key": "someThird",
        "defaultValue": "Some third value",
        "comboId": 1
      }
    ]
  }
]


The main section contains general properties of the combo box like weight, height, font, and font size.

A comboContent sub-section contains the content of the combo box, including all possible default values.

When UI invokes a metadata endpoint, it first should construct the page itself and then it should parse an example combobox.

Sample in React:

JavaScript
 
class SampleCombo extends Component {
    state = {
        metadata: null,
    }

    // process metadata
    componentDidMount() {
        const viewName = 'main';
        const self = this;
        axios.all([getMetadata(viewName), getMessageHeader(viewName)])
            .then(axios.spread((metadata, header) => {
                let formattedMetadata = formatMetadata(metadata);
                formattedMetadata = populateFields(header, formattedMetadata);
                self.setState({metadata: formattedMetadata, activeTab: formattedMetadata.sections.get('comboContent')});
            }));
    }

    // renders component
    render() {
        const {metadata, activeTab} = this.state;
        if (!metadata) return <Loader/>;
        const {
            codifier,
            font,
            fontSize,
            weight,
            height,
            displayable,
            immutable,
        } = metadata;
        return (
            <div id={uiName} className="klp-page">
               <select id="sample" name="sample" style="font={font};fontSize={fontSize};weight={weight};height={height}">
                  <option value="{key}">{defaultValue}</option>
               </select>
            </div>
        );
    }
}

NOTE: This example is not ideal, but it shows the main idea.

Database Schema

Library provides OOTB database schema tables whose goal is to provide base metadata common for all possible UIs. It consists of the following tables:

  • form_metadata
  • layout
  • view_field
  • lookup_holder
  • lookup_metadata
  • combo_box
  • combo_box_content
  • combo_box_and_content_relation

Screenshot of OOTB database schema tables.

Database Extension

It is possible to extend a database schema. For extension purposes and database version management purposes, a Liquibase is used. An out-of-the-box solution is written in XML representation, however, YAML representation is also acceptable per wish/requirements in each particular case.

Metadata Database Build (game engine)

Opinions expressed by DZone contributors are their own.

Related

  • Taming Billions of Rows: How Metadata and SQL Can Replace Your ETL Pipeline
  • Reading Table Metadata With Flight SQL
  • SeaweedFS vs. JuiceFS Design and Features
  • How To Approach Java, Databases, and SQL [Video]

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook