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
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Reading Table Metadata With Flight SQL
  • SeaweedFS vs. JuiceFS Design and Features
  • How To Approach Java, Databases, and SQL [Video]
  • ShardingSphere's Built-In Metadata Handling Function for Sharded Database Environments

Trending

  • Start Coding With Google Cloud Workstations
  • Automatic Code Transformation With OpenRewrite
  • Automating Data Pipelines: Generating PySpark and SQL Jobs With LLMs in Cloudera
  • The Human Side of Logs: What Unstructured Data Is Trying to Tell You
  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
9.1K 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

  • Reading Table Metadata With Flight SQL
  • SeaweedFS vs. JuiceFS Design and Features
  • How To Approach Java, Databases, and SQL [Video]
  • ShardingSphere's Built-In Metadata Handling Function for Sharded Database Environments

Partner Resources

×

Comments
Oops! Something Went Wrong

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

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

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 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!