Skip to content

Configuration File Splitting Analysis and Proposal (REVISED)

Current State Analysis

Configuration Structure

The current tourlinq-config.xml file (3150+ lines) uses JAXB annotations to define the entire application configuration in a single monolithic file.

Root Configuration Class: com.perun.tlinq.util.ClientConfig - Location: tqcommon/src/main/java/com/perun/tlinq/util/ClientConfig.java - Loads configuration from $TLINQ_HOME/tourlinq-config.xml - Uses JAXB to unmarshal into PluginConfig object

Configuration Hierarchy:

PluginConfig (root)
├── Plugins (PluginList)
│   └── Plugin[] - plugin definitions
├── Databases (DatabaseList)
│   └── Database[] - database connections
├── ServiceFactories (ServiceFactoryList)
│   └── ServiceFactory[] - backend service factories
│       ├── Properties
│       └── Services
├── AppProperties (PropertyList)
│   └── Property[] - application-wide properties
├── ScheduledJobList
│   └── ScheduledJob[] - scheduled tasks
└── Entities (EntityList) ← LARGEST SECTION
    └── Entity[] - entity configurations (majority of file size)
        └── EntityFactoryList
            └── Factory[] - factory-specific configurations
                ├── ServiceList - CRUD service mappings
                └── FieldMappingList - field mapping definitions
                    └── FieldMapping[] - individual field mappings

Current Loading Mechanism

File: ClientConfig.java:28-42

private ClientConfig() throws TlinqClientException {
    String path = System.getProperty("TLINQ_HOME");
    if(null == path)
        path = System.getenv("TLINQ_HOME");
    File configFile = new File(path+"/tourlinq-config.xml");

    try {
        JAXBContext ctx = JAXBContext.newInstance(PluginConfig.class);
        Unmarshaller unmarshaller = ctx.createUnmarshaller();
        config = (PluginConfig) unmarshaller.unmarshal(configFile);
    } catch (Throwable ex) {
        throw new TlinqClientException(ex);
    }
    configPath = path;
}

Problem Statement

  1. File Size: 3150+ lines in a single XML file
  2. Maintainability: Difficult to navigate and modify
  3. Merge Conflicts: Multiple developers editing the same file
  4. Modularity: Entities could be organized by domain/plugin
  5. Version Control: Hard to track changes to specific entities

CRITICAL CONSTRAINT: Multi-Factory Entities

The Challenge

Many canonical entities have mappings to MULTIPLE service factories. For example:

  • Product entity may have configurations for:
  • OdooServiceFactory
  • RaynaServiceFactory
  • AmadeusServiceFactory

  • Booking entity may have configurations for:

  • OdooServiceFactory
  • NTSServiceFactory

Current XML Structure:

<Entity name="Product" class="com.perun.tlinq.entity.CProduct" idField="productId">
    <EntityFactoryList>
        <Factory name="OdooServiceFactory" nativeEntity="com.perun.tlinq.client.odoo.entity.OdooProduct">
            <!-- Odoo-specific field mappings -->
        </Factory>
        <Factory name="RaynaServiceFactory" nativeEntity="com.perun.tlinq.client.rayna.entity.RaynaProduct">
            <!-- Rayna-specific field mappings -->
        </Factory>
        <Factory name="AmadeusServiceFactory" nativeEntity="com.perun.tlinq.client.amadeus.entity.AmadeusProduct">
            <!-- Amadeus-specific field mappings -->
        </Factory>
    </EntityFactoryList>
</Entity>

Why This Matters for Splitting Strategy

XInclude is NOT a merge mechanism - it's simple text replacement. If we split by plugin:

odoo-entities.xml:

<Entity name="Product" class="...">
    <EntityFactoryList>
        <Factory name="OdooServiceFactory">...</Factory>
    </EntityFactoryList>
</Entity>

rayna-entities.xml:

<Entity name="Product" class="...">
    <EntityFactoryList>
        <Factory name="RaynaServiceFactory">...</Factory>
    </EntityFactoryList>
</Entity>

Result after XInclude:

<Entities>
    <Entity name="Product">...</Entity>  <!-- Only Odoo -->
    <Entity name="Product">...</Entity>  <!-- Only Rayna -->
</Entities>

Problem: Two separate Entity elements with the same name! When ClientConfig.getEntityConfig("Product") searches the list, it returns the first match only, losing all other factory configurations.

This breaks the application completely.

Proposed Solutions

Given the multi-factory constraint, we have three viable approaches:

Overview

Split configuration files by entity domain (NOT by plugin), keeping all factory configurations for each entity together.

Directory Structure

$TLINQ_HOME/
├── tourlinq-config.xml          (main file)
├── config/
│   ├── plugins.xml              (plugin definitions)
│   ├── databases.xml            (database configurations)
│   ├── factories.xml            (service factory definitions)
│   ├── properties.xml           (application properties)
│   ├── scheduled-jobs.xml       (scheduled job definitions)
│   └── entities/
│       ├── user-entities.xml        (User, RegUser, UserContact - ALL factories)
│       ├── customer-entities.xml    (Customer, Company - ALL factories)
│       ├── product-entities.xml     (Product, ProductCategory - ALL factories)
│       ├── booking-entities.xml     (Booking, Reservation - ALL factories)
│       ├── payment-entities.xml     (Payment, Invoice - ALL factories)
│       ├── inventory-entities.xml   (Stock, Warehouse - ALL factories)
│       └── workflow-entities.xml    (WorkflowState, Approval - ALL factories)

Implementation

1. Enable XInclude Support in ClientConfig

Modify: ClientConfig.java

private ClientConfig() throws TlinqClientException {
    String path = System.getProperty("TLINQ_HOME");
    if(null == path)
        path = System.getenv("TLINQ_HOME");
    File configFile = new File(path+"/tourlinq-config.xml");

    try {
        // Create JAXB context
        JAXBContext ctx = JAXBContext.newInstance(PluginConfig.class);
        Unmarshaller unmarshaller = ctx.createUnmarshaller();

        // Enable XInclude processing
        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setNamespaceAware(true);
        spf.setXIncludeAware(true);  // Enable XInclude support

        XMLReader xmlReader = spf.newSAXParser().getXMLReader();
        SAXSource saxSource = new SAXSource(xmlReader,
            new InputSource(configFile.getAbsolutePath()));

        config = (PluginConfig) unmarshaller.unmarshal(saxSource);
    } catch (Throwable ex) {
        throw new TlinqClientException(ex);
    }
    configPath = path;
}

Required Imports:

import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;
import javax.xml.transform.sax.SAXSource;

2. Main Configuration File

File: tourlinq-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<PluginConfig xmlns:xi="http://www.w3.org/2001/XInclude">

    <!-- Plugin Definitions -->
    <xi:include href="config/plugins.xml" parse="xml">
        <xi:fallback>
            <Plugins/>
        </xi:fallback>
    </xi:include>

    <!-- Database Configurations -->
    <xi:include href="config/databases.xml" parse="xml">
        <xi:fallback>
            <Databases/>
        </xi:fallback>
    </xi:include>

    <!-- Service Factory Definitions -->
    <xi:include href="config/factories.xml" parse="xml">
        <xi:fallback>
            <ServiceFactories/>
        </xi:fallback>
    </xi:include>

    <!-- Application Properties -->
    <xi:include href="config/properties.xml" parse="xml">
        <xi:fallback>
            <AppProperties/>
        </xi:fallback>
    </xi:include>

    <!-- Scheduled Jobs -->
    <xi:include href="config/scheduled-jobs.xml" parse="xml">
        <xi:fallback>
            <ScheduledJobList/>
        </xi:fallback>
    </xi:include>

    <!-- Entity Configurations - Organized by Domain -->
    <Entities>
        <xi:include href="config/entities/user-entities.xml" parse="xml"/>
        <xi:include href="config/entities/customer-entities.xml" parse="xml"/>
        <xi:include href="config/entities/product-entities.xml" parse="xml"/>
        <xi:include href="config/entities/booking-entities.xml" parse="xml"/>
        <xi:include href="config/entities/payment-entities.xml" parse="xml"/>
        <xi:include href="config/entities/inventory-entities.xml" parse="xml"/>
        <xi:include href="config/entities/workflow-entities.xml" parse="xml"/>
    </Entities>

</PluginConfig>
3. Entity File Example - Product Entities

File: config/entities/product-entities.xml

<?xml version="1.0" encoding="UTF-8"?>
<!-- Product-related Entity Configurations -->

<!-- Product Entity - supports multiple factories -->
<Entity name="Product" class="com.perun.tlinq.entity.product.CProduct"
        idField="productId" defaultFactory="OdooServiceFactory">
    <EntityFactoryList>

        <!-- Odoo Factory Configuration -->
        <Factory name="OdooServiceFactory"
                 nativeEntity="com.perun.tlinq.client.odoo.entity.OdooProduct">
            <ServiceList>
                <Service name="searchProducts" action="search"/>
                <Service name="readProduct" action="read"/>
                <Service name="createProduct" action="create"/>
                <Service name="updateProduct" action="update"/>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="productId" sourceField="id" mapping="DirectMapping"/>
                <FieldMapping targetField="productName" sourceField="name" mapping="DirectMapping"/>
                <FieldMapping targetField="productCode" sourceField="default_code" mapping="DirectMapping"/>
                <FieldMapping targetField="description" sourceField="description" mapping="DirectMapping"/>
                <FieldMapping targetField="price" sourceField="list_price" mapping="DirectMapping"/>
                <!-- ... more Odoo-specific mappings ... -->
            </FieldMappingList>
        </Factory>

        <!-- Rayna Factory Configuration -->
        <Factory name="RaynaServiceFactory"
                 nativeEntity="com.perun.tlinq.client.rayna.entity.RaynaProduct">
            <ServiceList>
                <Service name="searchRaynaProducts" action="search"/>
                <Service name="getRaynaProduct" action="read"/>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="productId" sourceField="productId" mapping="DirectMapping"/>
                <FieldMapping targetField="productName" sourceField="productName" mapping="DirectMapping"/>
                <FieldMapping targetField="productCode" sourceField="sku" mapping="DirectMapping"/>
                <FieldMapping targetField="price" sourceField="netPrice" mapping="DirectMapping"/>
                <!-- ... more Rayna-specific mappings ... -->
            </FieldMappingList>
        </Factory>

        <!-- Amadeus Factory Configuration -->
        <Factory name="AmadeusServiceFactory"
                 nativeEntity="com.perun.tlinq.client.amadeus.entity.AmadeusProduct">
            <ServiceList>
                <Service name="searchFlights" action="search"/>
                <Service name="getFlightDetails" action="read"/>
            </ServiceList>
            <FieldMappingList>
                <FieldMapping targetField="productId" sourceField="flightId" mapping="DirectMapping"/>
                <FieldMapping targetField="productName" sourceField="flightNumber" mapping="DirectMapping"/>
                <!-- ... more Amadeus-specific mappings ... -->
            </FieldMappingList>
        </Factory>

    </EntityFactoryList>
</Entity>

<!-- ProductCategory Entity - may also support multiple factories -->
<Entity name="ProductCategory" class="com.perun.tlinq.entity.product.CProductCategory"
        idField="categoryId" defaultFactory="OdooServiceFactory">
    <EntityFactoryList>
        <Factory name="OdooServiceFactory"
                 nativeEntity="com.perun.tlinq.client.odoo.entity.OdooProductCategory">
            <!-- ... configuration ... -->
        </Factory>
    </EntityFactoryList>
</Entity>

<!-- Add more product-related entities here -->

Advantages

  1. No Merging Needed: XInclude simply concatenates entities, no duplicate entity names
  2. Simple Implementation: Only requires SAX parser configuration change
  3. Standard XML: Uses W3C XInclude standard
  4. Clear Organization: Entities grouped by business domain
  5. Easy to Locate: All factory configurations for an entity in one place
  6. Minimal Code Changes: Only ClientConfig.java modified

Disadvantages

  1. Not Plugin-Isolated: Can't separate configurations by plugin/vendor
  2. Large Domain Files: Some domain files might still be large if many entities
  3. Cross-Cutting Concerns: Plugin developers must edit domain files

Overview

Load separate configuration files and merge them programmatically, with intelligent handling of duplicate entity names by merging their factory lists.

Implementation

1. Extend EntityList with Smart Merge

Modify: EntityList.java

@XmlAccessorType(XmlAccessType.FIELD)
public class EntityList {

    @XmlElement(name="Entity")
    List<EntityConfig> entityList;

    // Existing methods...

    /**
     * Merge another EntityList into this one.
     * If an entity with the same name already exists, merge their factory lists.
     * Otherwise, add the entity as new.
     */
    public void merge(EntityList other) {
        if (other == null || other.entityList == null) {
            return;
        }

        if (this.entityList == null) {
            this.entityList = new ArrayList<>();
        }

        for (EntityConfig newEntity : other.entityList) {
            EntityConfig existing = findByName(newEntity.getEntityName());
            if (existing != null) {
                // Entity exists - merge factory lists
                mergeFactories(existing, newEntity);
            } else {
                // New entity - add it
                this.entityList.add(newEntity);
            }
        }
    }

    private EntityConfig findByName(String entityName) {
        if (entityList == null) return null;

        for (EntityConfig entity : entityList) {
            if (entityName.equals(entity.getEntityName())) {
                return entity;
            }
        }
        return null;
    }

    private void mergeFactories(EntityConfig target, EntityConfig source) {
        if (source.getFactoryList() == null) return;

        EntityFactoryList targetFactories = target.getFactoryList();
        if (targetFactories == null) {
            target.setFactoryList(source.getFactoryList());
            return;
        }

        // Add factories from source that don't exist in target
        for (EntityFactory sourceFactory : source.getFactoryList()) {
            if (!hasFactory(targetFactories, sourceFactory.getFactoryName())) {
                targetFactories.entityFactories.add(sourceFactory);
            } else {
                // Optional: log warning about duplicate factory
                Logger.getLogger(EntityList.class.getName())
                    .warning("Duplicate factory '" + sourceFactory.getFactoryName() +
                             "' for entity '" + target.getEntityName() + "' - skipping");
            }
        }
    }

    private boolean hasFactory(EntityFactoryList factories, String factoryName) {
        for (EntityFactory factory : factories.entityFactories) {
            if (factoryName.equals(factory.getFactoryName())) {
                return true;
            }
        }
        return false;
    }
}
2. Create Entity Configuration Loader

New Class: com.perun.tlinq.config.EntityConfigLoader

package com.perun.tlinq.config;

import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "EntityConfigurations")
@XmlAccessorType(XmlAccessType.FIELD)
public class EntityConfigLoader {

    @XmlElement(name = "Entities")
    EntityList entityList;

    public EntityList getEntityList() {
        return entityList;
    }

    public void setEntityList(EntityList entityList) {
        this.entityList = entityList;
    }
}
3. Update ClientConfig
private ClientConfig() throws TlinqClientException {
    String path = System.getProperty("TLINQ_HOME");
    if(null == path)
        path = System.getenv("TLINQ_HOME");

    try {
        // Load main configuration (may have skeleton entities or no entities)
        File configFile = new File(path+"/tourlinq-config.xml");
        JAXBContext ctx = JAXBContext.newInstance(PluginConfig.class);
        Unmarshaller unmarshaller = ctx.createUnmarshaller();
        config = (PluginConfig) unmarshaller.unmarshal(configFile);

        // Ensure entity list exists
        if (config.getEntityList() == null) {
            config.setEntityList(new EntityList());
        }

        // Load entity configurations from separate files
        File entitiesDir = new File(path+"/config/entities");
        if (entitiesDir.exists() && entitiesDir.isDirectory()) {
            JAXBContext entityCtx = JAXBContext.newInstance(EntityConfigLoader.class);
            Unmarshaller entityUnmarshaller = entityCtx.createUnmarshaller();

            // Sort files to ensure consistent loading order
            File[] entityFiles = entitiesDir.listFiles((dir, name) -> name.endsWith(".xml"));
            if (entityFiles != null) {
                Arrays.sort(entityFiles);

                for (File entityFile : entityFiles) {
                    logger.fine("Loading entity configuration from: " + entityFile.getName());
                    EntityConfigLoader loader = (EntityConfigLoader) entityUnmarshaller.unmarshal(entityFile);
                    config.getEntityList().merge(loader.getEntityList());
                }
            }
        }

        logger.info("Loaded " + config.getEntityList().getEntities().size() + " entity configurations");
    } catch (Throwable ex) {
        throw new TlinqClientException(ex);
    }
    configPath = path;
}
4. Directory Structure - Plugin-Based Organization
$TLINQ_HOME/
├── tourlinq-config.xml          (main file - no entities or just common ones)
├── config/
│   ├── plugins.xml
│   ├── databases.xml
│   ├── factories.xml
│   ├── properties.xml
│   ├── scheduled-jobs.xml
│   └── entities/
│       ├── 01-common-entities.xml     (Common entities used by all plugins)
│       ├── 10-odoo-entities.xml       (Odoo factory configurations)
│       ├── 20-rayna-entities.xml      (Rayna factory configurations)
│       ├── 30-amadeus-entities.xml    (Amadeus factory configurations)
│       └── 40-nts-entities.xml        (NTS factory configurations)

Note: Files are numbered to control loading order if needed.

5. Entity File Example - Plugin-Based

File: config/entities/10-odoo-entities.xml

<?xml version="1.0" encoding="UTF-8"?>
<EntityConfigurations>
    <Entities>

        <!-- Product entity - Odoo factory only -->
        <Entity name="Product" class="com.perun.tlinq.entity.product.CProduct"
                idField="productId" defaultFactory="OdooServiceFactory">
            <EntityFactoryList>
                <Factory name="OdooServiceFactory"
                         nativeEntity="com.perun.tlinq.client.odoo.entity.OdooProduct">
                    <ServiceList>
                        <Service name="searchProducts" action="search"/>
                        <Service name="readProduct" action="read"/>
                        <!-- ... -->
                    </ServiceList>
                    <FieldMappingList>
                        <FieldMapping targetField="productId" sourceField="id" mapping="DirectMapping"/>
                        <!-- ... Odoo-specific mappings ... -->
                    </FieldMappingList>
                </Factory>
            </EntityFactoryList>
        </Entity>

        <!-- Customer entity - Odoo factory only -->
        <Entity name="Customer" class="com.perun.tlinq.entity.customer.CCustomer"
                idField="customerId" defaultFactory="OdooServiceFactory">
            <EntityFactoryList>
                <Factory name="OdooServiceFactory"
                         nativeEntity="com.perun.tlinq.client.odoo.entity.OdooCustomer">
                    <!-- ... Odoo configuration ... -->
                </Factory>
            </EntityFactoryList>
        </Entity>

        <!-- Add more Odoo-specific entity configurations -->

    </Entities>
</EntityConfigurations>

File: config/entities/20-rayna-entities.xml

<?xml version="1.0" encoding="UTF-8"?>
<EntityConfigurations>
    <Entities>

        <!-- Product entity - Rayna factory only -->
        <!-- Will be MERGED with the Odoo Product configuration -->
        <Entity name="Product" class="com.perun.tlinq.entity.product.CProduct"
                idField="productId">
            <EntityFactoryList>
                <Factory name="RaynaServiceFactory"
                         nativeEntity="com.perun.tlinq.client.rayna.entity.RaynaProduct">
                    <ServiceList>
                        <Service name="searchRaynaProducts" action="search"/>
                        <!-- ... -->
                    </ServiceList>
                    <FieldMappingList>
                        <FieldMapping targetField="productId" sourceField="productId" mapping="DirectMapping"/>
                        <!-- ... Rayna-specific mappings ... -->
                    </FieldMappingList>
                </Factory>
            </EntityFactoryList>
        </Entity>

        <!-- Add more Rayna-specific entity configurations -->

    </Entities>
</EntityConfigurations>

Result after merge: Product entity will have BOTH OdooServiceFactory and RaynaServiceFactory in its EntityFactoryList.

Advantages

  1. Plugin Isolation: Each plugin maintains its own entity configuration file
  2. Smart Merging: Automatically combines factories for entities with same name
  3. Flexible Organization: Can organize by plugin, domain, or any other criteria
  4. Incremental Loading: Easy to add/remove plugin configurations
  5. Plugin Development: Plugin developers only touch their own files
  6. No Merge Conflicts: Different teams work on different files

Disadvantages

  1. More Complex Code: Need to maintain merge logic
  2. Multiple JAXB Contexts: Requires separate unmarshalling for entity files
  3. Load Order Sensitivity: May need to control file loading order
  4. Testing Overhead: Must test merge logic thoroughly
  5. Duplicate Detection: Need to decide how to handle duplicate factories

Option 3: Hybrid - Factory-Level XInclude

Overview

Use XInclude at the Factory level within each entity definition.

Example Structure

Main entity file: config/entities/product-entity.xml

<?xml version="1.0" encoding="UTF-8"?>
<Entity name="Product" class="com.perun.tlinq.entity.product.CProduct"
        idField="productId" defaultFactory="OdooServiceFactory"
        xmlns:xi="http://www.w3.org/2001/XInclude">
    <EntityFactoryList>
        <xi:include href="../factories/product-odoo-factory.xml" parse="xml"/>
        <xi:include href="../factories/product-rayna-factory.xml" parse="xml"/>
        <xi:include href="../factories/product-amadeus-factory.xml" parse="xml"/>
    </EntityFactoryList>
</Entity>

Factory file: config/factories/product-odoo-factory.xml

<?xml version="1.0" encoding="UTF-8"?>
<Factory name="OdooServiceFactory"
         nativeEntity="com.perun.tlinq.client.odoo.entity.OdooProduct">
    <ServiceList>
        <Service name="searchProducts" action="search"/>
        <!-- ... -->
    </ServiceList>
    <FieldMappingList>
        <FieldMapping targetField="productId" sourceField="id" mapping="DirectMapping"/>
        <!-- ... -->
    </FieldMappingList>
</Factory>

Advantages

  1. Fine-Grained Splitting: Each factory configuration in separate file
  2. Plugin Isolation: Plugin teams manage their factory files
  3. XInclude Benefits: Uses standard mechanism
  4. Explicit Dependencies: Entity file shows which factories it supports

Disadvantages

  1. Many Small Files: Could result in hundreds of small files
  2. Complex Structure: Three-level file hierarchy
  3. Harder Navigation: Need to jump between many files
  4. Entity File Complexity: Each entity needs XInclude directives

Recommendation

Primary Recommendation: Option 2 (Programmatic Merging with Smart Deduplication)

Rationale

  1. Plugin Isolation: Each plugin team can maintain their own entity configuration files
  2. Handles Multi-Factory: Smart merge properly combines factories for same entity
  3. Flexible Organization: Can organize files by plugin, domain, or any criteria
  4. Minimal Merge Conflicts: Different teams work on different files
  5. Extensible: Easy to add new plugins without touching existing files
  6. Clear Ownership: Plugin developers own their entity mappings

Alternative Recommendation: Option 1 (Domain-Based XInclude)

If plugin isolation is not a priority and you prefer simplicity: - Simpler implementation - No merge logic needed - Standard XInclude approach - Organizes by business domain

Why Not Option 3?

  • Too many small files
  • Complex three-level structure
  • Harder to maintain and navigate

Migration Strategy

Phase 1: Implement Merge Logic

  1. Add smart merge methods to EntityList.java
  2. Create EntityConfigLoader.java class
  3. Update ClientConfig.java to load and merge entity files
  4. Add unit tests for merge logic

Phase 2: Create Directory Structure

  1. Create $TLINQ_HOME/config/ directory
  2. Create config/entities/ subdirectory
  3. Set up version control

Phase 3: Split Non-Entity Configuration

  1. Extract plugins to config/plugins.xml
  2. Extract databases to config/databases.xml
  3. Extract factories to config/factories.xml
  4. Extract properties to config/properties.xml
  5. Extract scheduled jobs to config/scheduled-jobs.xml
  6. Update main tourlinq-config.xml to use XInclude for these sections

Phase 4: Split Entity Configuration by Plugin

  1. Create config/entities/01-common-entities.xml for shared entities
  2. Create config/entities/10-odoo-entities.xml for Odoo factory configurations
  3. Create config/entities/20-rayna-entities.xml for Rayna factory configurations
  4. Create config/entities/30-amadeus-entities.xml for Amadeus configurations
  5. Create config/entities/40-nts-entities.xml for NTS configurations

Phase 5: Extract Entity Definitions

  1. For each entity in the monolithic file:
  2. Determine which plugins/factories it supports
  3. Create separate <Entity> definitions in each plugin file
  4. Include only the relevant factory in each file
  5. Remove <Entities> section from main config (or leave empty)

Phase 6: Testing

  1. Unit test merge logic with various scenarios:
  2. Single factory entities
  3. Multi-factory entities
  4. Duplicate factory detection
  5. Empty entity lists
  6. Integration test full configuration loading
  7. Verify entity counts and factory availability
  8. Test entity operations (search, read, write, delete)

Phase 7: Deployment

  1. Deploy to development environment
  2. Run comprehensive integration tests
  3. Deploy to test environment
  4. Validate all functionality
  5. Deploy to production with rollback plan

For Option 1 (Domain-Based XInclude) - ALTERNATIVE

Similar to above, but: - Phase 1: Only implement XInclude support (simpler) - Phase 4: Split by domain instead of plugin - user-entities.xml - all user entities with ALL factories - product-entities.xml - all product entities with ALL factories - etc. - No merge logic needed

Testing Considerations

Unit Tests for Merge Logic (Option 2)

@Test
public void testMergeSingleEntity() {
    EntityList list1 = new EntityList();
    EntityConfig entity1 = createEntity("Product", "OdooServiceFactory");
    list1.addEntity(entity1);

    EntityList list2 = new EntityList();
    EntityConfig entity2 = createEntity("Product", "RaynaServiceFactory");
    list2.addEntity(entity2);

    list1.merge(list2);

    assertEquals(1, list1.getEntities().size());
    EntityConfig merged = (EntityConfig) list1.getEntities().get(0);
    assertEquals(2, merged.getFactoryList().size());
}

@Test
public void testMergeDuplicateFactory() {
    EntityList list1 = new EntityList();
    EntityConfig entity1 = createEntity("Product", "OdooServiceFactory");
    list1.addEntity(entity1);

    EntityList list2 = new EntityList();
    EntityConfig entity2 = createEntity("Product", "OdooServiceFactory");
    list2.addEntity(entity2);

    list1.merge(list2);

    assertEquals(1, list1.getEntities().size());
    EntityConfig merged = (EntityConfig) list1.getEntities().get(0);
    assertEquals(1, merged.getFactoryList().size()); // Duplicate not added
}

@Test
public void testMergeDifferentEntities() {
    EntityList list1 = new EntityList();
    list1.addEntity(createEntity("Product", "OdooServiceFactory"));

    EntityList list2 = new EntityList();
    list2.addEntity(createEntity("Customer", "OdooServiceFactory"));

    list1.merge(list2);

    assertEquals(2, list1.getEntities().size());
}

Integration Tests

@Test
public void testConfigurationLoading() throws TlinqClientException {
    ClientConfig config = ClientConfig.instance();
    assertNotNull(config.getConfig());
    assertNotNull(config.getEntities());
    assertTrue(config.getEntities().size() > 0);
}

@Test
public void testMultiFactoryEntity() throws TlinqClientException {
    ClientConfig config = ClientConfig.instance();
    EntityConfig productEntity = config.getEntityConfig("Product");
    assertNotNull(productEntity);

    // Verify multiple factories loaded
    assertTrue(productEntity.getFactoryList().size() > 1);

    // Verify specific factories exist
    assertNotNull(productEntity.getFactory("OdooServiceFactory"));
    assertNotNull(productEntity.getFactory("RaynaServiceFactory"));
}

@Test
public void testEntityFactoryResolution() throws TlinqClientException {
    ClientConfig config = ClientConfig.instance();
    EntityConfig entity = config.getEntityConfig("Product");

    // Test factory-specific service retrieval
    EntityServiceConfig odooSearch = entity.getSearchService("OdooServiceFactory");
    assertNotNull(odooSearch);

    EntityServiceConfig raynaSearch = entity.getSearchService("RaynaServiceFactory");
    assertNotNull(raynaSearch);
}

Error Handling

Missing Files

// In ClientConfig constructor
for (File entityFile : entityFiles) {
    try {
        logger.fine("Loading entity configuration from: " + entityFile.getName());
        EntityConfigLoader loader = (EntityConfigLoader) entityUnmarshaller.unmarshal(entityFile);
        config.getEntityList().merge(loader.getEntityList());
    } catch (JAXBException ex) {
        logger.warning("Failed to load entity configuration from " +
                      entityFile.getName() + ": " + ex.getMessage());
        // Continue loading other files or throw exception based on policy
    }
}

Validation

Add validation to ensure: 1. No duplicate factories within the same file 2. Entity class names are consistent across files 3. Default factory is specified and exists 4. Required fields are present

private void validateEntityConfig(EntityConfig entity) throws TlinqClientException {
    if (entity.getEntityName() == null || entity.getEntityName().isEmpty()) {
        throw new TlinqClientException("Entity configuration missing name");
    }

    if (entity.getEntityClass() == null || entity.getEntityClass().isEmpty()) {
        throw new TlinqClientException("Entity " + entity.getEntityName() +
                                      " missing class attribute");
    }

    if (entity.getDefaultFactory() == null || entity.getDefaultFactory().isEmpty()) {
        throw new TlinqClientException("Entity " + entity.getEntityName() +
                                      " missing defaultFactory attribute");
    }
}

Additional Benefits

  1. Team Collaboration: Plugin teams work independently on their configurations
  2. Clear Ownership: Each plugin owns its entity mapping files
  3. Easier Code Reviews: Smaller, focused configuration changes in PRs
  4. Selective Loading: Future enhancement to conditionally load plugin configurations
  5. Plugin Development: New plugins just add new configuration files
  6. Maintenance: Easy to locate and update plugin-specific configurations
  7. Testing: Can test plugin configurations in isolation

Conclusion

Given the critical requirement that entities can have multiple factory configurations, the recommended approach is Option 2: Programmatic Merging with Smart Deduplication.

This approach: - Properly handles multi-factory entities by merging their factory lists - Allows plugin-based organization for better team collaboration - Provides flexibility in file organization - Requires moderate code changes but provides significant long-term benefits - Enables independent plugin development

The implementation involves: 1. Adding smart merge logic to EntityList 2. Creating EntityConfigLoader for loading entity files 3. Updating ClientConfig to load and merge entity configurations 4. Organizing configuration files by plugin for clear ownership

While XInclude (Option 1) is simpler to implement, organizing by domain instead of plugin may not align with team structure and plugin ownership models. However, it remains a viable alternative if simplicity is prioritized over plugin isolation.