TQPRO Entity Management Guide¶
Version: 1.0 Last Updated: 2025-11-19 Author: TourLinq Development Team
Table of Contents¶
- Introduction
- Entity Management Concept
- Architecture Overview
- Entity Configuration System
- Creating a New Entity - Step-by-Step Guide
- Advanced Topics
- Best Practices
- Examples
- Troubleshooting
- Reference
Introduction¶
TQPRO (TourLinq Professional) is a platform for managing travel and tourism business operations. At its core is a flexible entity management system that provides a unified interface for working with diverse business objects such as customers, flights, hotels, cruises, and products.
This guide explains the entity management concept in TQPRO and provides detailed instructions for creating new entities within the framework.
Key Features¶
- Unified Entity Framework: All business objects inherit from a common base class
- Configuration-Driven: Entity behavior defined in XML configuration files
- Service Factory Pattern: Pluggable backends for different data sources
- Field Mapping System: Automatic translation between native and TQPRO entities
- Facade Pattern: Simplified API for entity operations (CRUD)
- Multi-Backend Support: Single entity can work with multiple service factories
Entity Management Concept¶
What is an Entity?¶
In TQPRO, an entity represents a business domain object - a customer, hotel, flight, product, etc. Entities serve as:
- Data Transfer Objects (DTOs): Carrying data between application layers
- Domain Models: Representing business concepts
- Integration Adapters: Bridging TQPRO and external systems (Odoo, Amadeus, Rayna, etc.)
The Three-Layer Architecture¶
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (Controllers, Services) │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Facade Layer │
│ (CustomerFacade, ProductFacade, etc.) │
│ - High-level API for entity operations │
│ - CRUD operations (create, read, update, delete) │
│ - Business logic coordination │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Entity Layer │
│ (CCustomer, CProduct, CFlightOffer, etc.) │
│ - Pure data objects (POJOs) │
│ - Field getters and setters │
│ - Extends TlinqEntity │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Service Factory Layer │
│ (OdooServiceFactory, AmadeusServiceFactory) │
│ - Backend-specific implementations │
│ - Field mapping and translation │
│ - Native API integration │
└─────────────────────────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ External Systems │
│ (Odoo, Amadeus, Rayna B2B, NTS, etc.) │
└─────────────────────────────────────────────────────────────┘
Core Concepts¶
1. Entity Classes¶
Entity classes extend TlinqEntity and represent business objects:
public class CCustomer extends TlinqEntity implements CustomerI {
protected Integer customerId;
protected String custName;
protected String email;
// ... more fields with getters/setters
}
2. Entity Configuration¶
Entities are configured in XML files that define: - Entity metadata (name, class, ID field) - Service factories that can handle the entity - Service operations (search, read, create, update, delete) - Field mappings between TQPRO and native formats
3. Service Factories¶
Service factories provide backend-specific implementations:
- OdooServiceFactory - Odoo ERP integration
- AmadeusServiceFactory - Amadeus GDS integration
- RaynaB2BServiceFactory - Rayna B2B API integration
- NTSServiceFactory - NTS database integration
4. Facades¶
Facades provide a simplified API for working with entities:
CustomerFacade facade = new CustomerFacade(securityToken);
facade.loadCustomer(customerId);
CCustomer customer = facade.getCustomer();
customer.setEmail("newemail@example.com");
facade.save();
Architecture Overview¶
Class Hierarchy¶
AbstractEntity (base)
↑
TlinqEntity (framework integration)
↑
CCustomer, CProduct, CFlight, etc. (domain entities)
AbstractEntity¶
Base class providing:
- Serialization support
- Field reflection utilities
- Entity initialization from native objects
- JSON serialization (toString())
- Annotation-based field mapping
Key Methods:
- initFromEntity(Object ent) - Initialize from native entity
- listEntityFields() - Get all entity fields via reflection
- toString() - JSON representation
TlinqEntity¶
Framework integration layer providing:
- Factory name tracking
- Lifecycle hooks (preLoad(), postLoad())
- Factory context management
Domain Entities¶
Specific business objects like CCustomer, CProduct, etc.
Characteristics:
- Extend TlinqEntity
- Plain Java objects (POJOs) with fields and getters/setters
- May implement domain interfaces (e.g., CustomerI)
- Optional annotations for field mapping
Entity Configuration System¶
Configuration Structure¶
Entity configurations are stored in XML files organized by domain:
config/
├── tourlinq-config.xml # Main config with XInclude references
└── entities/
├── customer-entities.xml # Customer domain entities
├── product-entities.xml # Product domain entities
├── flight-entities.xml # Flight domain entities
└── ... (more domain files)
Entity Configuration Format¶
<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">
<ServiceList>
<Service name="searchCustomers" action="search"/>
<Service name="getCustomers" action="read"/>
<Service name="createCustomer" action="create"/>
<Service name="updateCustomer" action="update"/>
</ServiceList>
<FieldMappingList>
<FieldMapping targetField="customerId"
sourceField="id"
mapping="DirectMapping"/>
<FieldMapping targetField="custName"
sourceField="name"
mapping="DirectMapping"/>
<!-- More field mappings -->
</FieldMappingList>
</Factory>
</EntityFactoryList>
</Entity>
Configuration Elements¶
Entity Element¶
| Attribute | Description | Required |
|---|---|---|
name |
Entity name (used in code) | Yes |
class |
Fully qualified Java class name | Yes |
idField |
Name of the ID field | Yes |
defaultFactory |
Default service factory to use | Yes |
Factory Element¶
| Attribute | Description | Required |
|---|---|---|
name |
Service factory name | Yes |
nativeEntity |
Native entity class (backend-specific) | No |
Service Element¶
| Attribute | Description | Required |
|---|---|---|
name |
Service method name | Yes |
action |
CRUD action (search/read/create/update/delete) or custom | Yes |
returnClass |
Return type for the service | No |
FieldMapping Element¶
| Attribute | Description | Required |
|---|---|---|
targetField |
TQPRO entity field name | Yes |
sourceField |
Native entity field name | Yes |
mapping |
Mapping type (DirectMapping, ArrayMapping, etc.) | Yes |
reverseMapping |
Reverse mapping type | No |
targetFieldEntity |
Target entity for nested mappings | No |
Mapping Types¶
| Type | Description | Example Use Case |
|---|---|---|
| DirectMapping | Simple 1:1 field mapping | customerId ↔ id |
| ArrayMapping | Collection/array mapping | contacts[] ↔ contact_ids[] |
| NestedMapping | Complex object relationships | Customer → Address mapping |
| IndexMapping | Reference by ID/index | countryId ↔ country_id[0] |
| NoMapping | Virtual/computed field (one-way) | fullName (computed from first + last) |
Creating a New Entity - Step-by-Step Guide¶
This section provides a comprehensive guide to creating a new entity in TQPRO.
Step 1: Define the Business Requirements¶
Before creating an entity, answer these questions:
- What business object does this represent? (e.g., Visa Application, Hotel Booking)
- What data fields are needed? (name, ID, dates, prices, etc.)
- Which backend system will provide the data? (Odoo, Amadeus, custom API)
- What operations are needed? (search, create, update, delete)
- How does it relate to other entities? (belongs to Customer, contains Items, etc.)
Step 2: Create the Entity Class¶
Create a new Java class in the appropriate package:
Package Structure:
Example: Creating a Visa Application Entity
package com.perun.tlinq.entity.visa;
import com.perun.tlinq.entity.TlinqEntity;
import java.time.LocalDate;
/**
* Visa Application entity
* Represents a visa application request
*/
public class CVisaApplication extends TlinqEntity {
// Primary key
protected Integer applicationId;
// Basic information
protected String applicationNumber;
protected String applicantName;
protected String passportNumber;
protected String nationality;
// Application details
protected String visaType;
protected String visaCategory;
protected String destinationCountry;
protected LocalDate travelDate;
protected Integer durationDays;
// Status
protected String status;
protected LocalDate submissionDate;
protected LocalDate approvalDate;
// Relationships
protected Integer customerId;
protected String customerName;
// Cost
protected Double applicationFee;
protected Double processingFee;
protected Double totalAmount;
// Documents
protected String[] requiredDocuments;
protected String[] submittedDocuments;
// Constructor
public CVisaApplication() {
super();
}
// Getters and Setters
public Integer getApplicationId() {
return applicationId;
}
public void setApplicationId(Integer applicationId) {
this.applicationId = applicationId;
}
public String getApplicationNumber() {
return applicationNumber;
}
public void setApplicationNumber(String applicationNumber) {
this.applicationNumber = applicationNumber;
}
public String getApplicantName() {
return applicantName;
}
public void setApplicantName(String applicantName) {
this.applicantName = applicantName;
}
// ... continue with all getters and setters
public String[] getRequiredDocuments() {
return requiredDocuments;
}
public void setRequiredDocuments(String[] requiredDocuments) {
this.requiredDocuments = requiredDocuments;
}
// Optional: Override lifecycle hooks
@Override
public void preLoad() {
// Called before entity is loaded from backend
super.preLoad();
// Custom logic here
}
@Override
public void postLoad() {
// Called after entity is loaded from backend
super.postLoad();
// Custom logic here (e.g., calculate totalAmount)
if (applicationFee != null && processingFee != null) {
totalAmount = applicationFee + processingFee;
}
}
}
Key Points:
- Extend TlinqEntity
- Use protected fields
- Follow naming convention: C[EntityName]
- Provide getters and setters for all fields
- Add JavaDoc comments
- Use appropriate data types (Integer for IDs, LocalDate for dates, etc.)
Step 3: Create the Entity Configuration¶
Add the entity configuration to the appropriate domain XML file:
File: config/entities/visa-entities.xml
<Entity name="VisaApplication"
class="com.perun.tlinq.entity.visa.CVisaApplication"
idField="applicationId"
defaultFactory="OdooServiceFactory">
<EntityFactoryList>
<Factory name="OdooServiceFactory"
nativeEntity="com.perun.tlinq.client.odoo.entity.OdooVisaApplication">
<ServiceList>
<!-- CRUD operations -->
<Service name="searchVisaApplications" action="search"/>
<Service name="getVisaApplication" action="read"/>
<Service name="createVisaApplication" action="create"/>
<Service name="updateVisaApplication" action="update"/>
<Service name="deleteVisaApplication" action="delete"/>
<!-- Custom operations -->
<Service name="submitForApproval" action="submit"/>
<Service name="approveApplication" action="approve"/>
<Service name="rejectApplication" action="reject"/>
</ServiceList>
<FieldMappingList>
<!-- ID fields -->
<FieldMapping targetField="applicationId"
sourceField="id"
mapping="DirectMapping"/>
<!-- Basic information -->
<FieldMapping targetField="applicationNumber"
sourceField="application_number"
mapping="DirectMapping"/>
<FieldMapping targetField="applicantName"
sourceField="applicant_name"
mapping="DirectMapping"/>
<FieldMapping targetField="passportNumber"
sourceField="passport_number"
mapping="DirectMapping"/>
<FieldMapping targetField="nationality"
sourceField="nationality"
mapping="DirectMapping"/>
<!-- Application details -->
<FieldMapping targetField="visaType"
sourceField="visa_type"
mapping="DirectMapping"/>
<FieldMapping targetField="visaCategory"
sourceField="visa_category"
mapping="DirectMapping"/>
<FieldMapping targetField="destinationCountry"
sourceField="destination_country"
mapping="DirectMapping"/>
<FieldMapping targetField="travelDate"
sourceField="travel_date"
mapping="DirectMapping"/>
<FieldMapping targetField="durationDays"
sourceField="duration_days"
mapping="DirectMapping"/>
<!-- Status fields -->
<FieldMapping targetField="status"
sourceField="state"
mapping="DirectMapping"/>
<FieldMapping targetField="submissionDate"
sourceField="submission_date"
mapping="DirectMapping"/>
<FieldMapping targetField="approvalDate"
sourceField="approval_date"
mapping="DirectMapping"/>
<!-- Customer relationship -->
<FieldMapping targetField="customerId"
sourceField="partner_id"
mapping="IndexMapping"/>
<FieldMapping targetField="customerName"
sourceField="partner_name"
mapping="DirectMapping"
reverseMapping="NoMapping"/>
<!-- Financial fields -->
<FieldMapping targetField="applicationFee"
sourceField="application_fee"
mapping="DirectMapping"/>
<FieldMapping targetField="processingFee"
sourceField="processing_fee"
mapping="DirectMapping"/>
<FieldMapping targetField="totalAmount"
sourceField="total_amount"
mapping="DirectMapping"
reverseMapping="NoMapping"/>
<!-- Document arrays -->
<FieldMapping targetField="requiredDocuments"
sourceField="required_document_ids"
mapping="ArrayMapping"/>
<FieldMapping targetField="submittedDocuments"
sourceField="submitted_document_ids"
mapping="ArrayMapping"/>
</FieldMappingList>
</Factory>
</EntityFactoryList>
</Entity>
Configuration Guidelines:
- Use descriptive service names
- Map all entity fields
- Use appropriate mapping types
- Set reverseMapping="NoMapping" for read-only/computed fields
- Include custom actions beyond CRUD if needed
Step 4: Create the Native Entity (Backend-Specific)¶
Create the native entity class that represents the backend format:
File: com/perun/tlinq/client/odoo/entity/OdooVisaApplication.java
package com.perun.tlinq.client.odoo.entity;
import com.perun.tlinq.client.odoo.OdooEntity;
/**
* Native Odoo entity for visa applications
*/
public class OdooVisaApplication extends OdooEntity {
public Integer id;
public String application_number;
public String applicant_name;
public String passport_number;
public String nationality;
public String visa_type;
public String visa_category;
public String destination_country;
public String travel_date;
public Integer duration_days;
public String state;
public String submission_date;
public String approval_date;
public Integer[] partner_id; // Odoo uses ID arrays for many2one
public String partner_name;
public Double application_fee;
public Double processing_fee;
public Double total_amount;
public Integer[] required_document_ids;
public Integer[] submitted_document_ids;
// Constructor
public OdooVisaApplication() {
super();
}
}
Native Entity Notes:
- Match backend field names exactly
- Use backend data types (Odoo uses Integer[] for references)
- Extends framework-specific base class (OdooEntity, AmadeusEntity, etc.)
Step 5: Implement Service Methods¶
Add service methods to the appropriate service factory:
File: com/perun/tlinq/client/odoo/service/OdooServiceFactory.java
/**
* Search for visa applications
*/
public List<CVisaApplication> searchVisaApplications(
String token,
SelectCriteriaList criteria) throws TlinqClientException {
// Call Odoo API
OdooVisaApplication[] results = odooClient.searchRead(
"visa.application",
criteria.toOdooDomain(),
OdooVisaApplication.class
);
// Map to TQPRO entities
List<CVisaApplication> applications = new ArrayList<>();
for (OdooVisaApplication native : results) {
CVisaApplication app = new CVisaApplication();
entityMapper.map(native, app);
applications.add(app);
}
return applications;
}
/**
* Get visa application by ID
*/
public CVisaApplication getVisaApplication(
String token,
Integer id) throws TlinqClientException {
OdooVisaApplication native = odooClient.read(
"visa.application",
id,
OdooVisaApplication.class
);
CVisaApplication app = new CVisaApplication();
entityMapper.map(native, app);
return app;
}
/**
* Create visa application
*/
public CVisaApplication createVisaApplication(
String token,
CVisaApplication application) throws TlinqClientException {
OdooVisaApplication native = new OdooVisaApplication();
entityMapper.mapReverse(application, native);
Integer id = odooClient.create("visa.application", native);
return getVisaApplication(token, id);
}
/**
* Update visa application
*/
public CVisaApplication updateVisaApplication(
String token,
CVisaApplication application) throws TlinqClientException {
OdooVisaApplication native = new OdooVisaApplication();
entityMapper.mapReverse(application, native);
odooClient.update(
"visa.application",
application.getApplicationId(),
native
);
return getVisaApplication(token, application.getApplicationId());
}
/**
* Custom action: Submit application for approval
*/
public CVisaApplication submitForApproval(
String token,
Integer id) throws TlinqClientException {
odooClient.callMethod(
"visa.application",
"action_submit",
new Object[]{id}
);
return getVisaApplication(token, id);
}
Step 6: Create the Facade (Optional but Recommended)¶
Create a facade to simplify entity operations:
File: com/perun/tlinq/entity/visa/VisaApplicationFacade.java
package com.perun.tlinq.entity.visa;
import com.perun.tlinq.config.SelectCriteriaList;
import com.perun.tlinq.entity.EntityFacade;
import com.perun.tlinq.util.TlinqClientException;
import java.util.List;
/**
* Facade for VisaApplication entity operations
*/
public class VisaApplicationFacade extends EntityFacade {
private CVisaApplication application;
private static final String ENTITY_NAME = "VisaApplication";
// Constructors
public VisaApplicationFacade(String token) {
super(token);
}
public VisaApplicationFacade(String token, boolean initializeNew) {
super(token);
if (initializeNew) {
application = new CVisaApplication();
}
}
public VisaApplicationFacade(String token, Integer applicationId)
throws TlinqClientException {
super(token);
application = findById(applicationId);
}
public VisaApplicationFacade(String token, CVisaApplication app) {
super(token);
this.application = app;
}
// CRUD Operations
/**
* Save the current application (create or update)
*/
public void save() throws TlinqClientException {
if (application == null) {
throw new TlinqClientException("Application not initialized");
}
if (application.getApplicationId() != null) {
application = update(application);
} else {
application = create(application);
}
}
/**
* Load application by ID
*/
public void load(Integer id) throws TlinqClientException {
application = findById(id);
}
/**
* Delete current application
*/
public void delete() throws TlinqClientException {
if (application == null || application.getApplicationId() == null) {
throw new TlinqClientException("Cannot delete unsaved application");
}
deleteById(application.getApplicationId());
application = null;
}
/**
* Search for applications
*/
public List<CVisaApplication> search(SelectCriteriaList criteria)
throws TlinqClientException {
return (List<CVisaApplication>) searchEntities(ENTITY_NAME, criteria);
}
// Helper Methods
private CVisaApplication findById(Integer id) throws TlinqClientException {
return (CVisaApplication) getEntityFactory()
.readEntity(getToken(), ENTITY_NAME, id);
}
private CVisaApplication create(CVisaApplication app)
throws TlinqClientException {
return (CVisaApplication) getEntityFactory()
.createEntity(getToken(), ENTITY_NAME, app);
}
private CVisaApplication update(CVisaApplication app)
throws TlinqClientException {
return (CVisaApplication) getEntityFactory()
.updateEntity(getToken(), ENTITY_NAME, app);
}
private void deleteById(Integer id) throws TlinqClientException {
getEntityFactory().deleteEntity(getToken(), ENTITY_NAME, id);
}
// Business Methods
/**
* Submit application for approval
*/
public void submitForApproval() throws TlinqClientException {
if (application == null || application.getApplicationId() == null) {
throw new TlinqClientException("Cannot submit unsaved application");
}
// Call custom service action
application = (CVisaApplication) getEntityFactory()
.executeCustomAction(
getToken(),
ENTITY_NAME,
"submit",
application.getApplicationId()
);
}
/**
* Check if application is approved
*/
public boolean isApproved() {
return application != null && "approved".equals(application.getStatus());
}
/**
* Calculate total cost
*/
public Double calculateTotalCost() {
if (application == null) return 0.0;
Double total = 0.0;
if (application.getApplicationFee() != null) {
total += application.getApplicationFee();
}
if (application.getProcessingFee() != null) {
total += application.getProcessingFee();
}
return total;
}
// Getters and Setters
public CVisaApplication getApplication() {
return application;
}
public void setApplication(CVisaApplication application) {
this.application = application;
}
}
Step 7: Update Configuration Index¶
If you created a new domain file, update the main configuration:
File: config/tourlinq-config.xml
<Entities>
<!-- ... existing includes ... -->
<!-- Visa entities -->
<xi:include href="entities/visa-entities.xml" xpointer="xpointer(/Entities/*)"/>
<!-- ... more includes ... -->
</Entities>
Step 8: Test the Entity¶
Create a test class to verify the entity works correctly:
File: test/.../visa/VisaApplicationTest.java
package com.perun.tlinq.entity.visa;
import com.perun.tlinq.util.TlinqClientException;
import org.junit.Test;
import static org.junit.Assert.*;
public class VisaApplicationTest {
@Test
public void testCreateVisaApplication() throws TlinqClientException {
String token = getTestToken();
// Create new application
VisaApplicationFacade facade = new VisaApplicationFacade(token, true);
CVisaApplication app = facade.getApplication();
app.setApplicantName("John Doe");
app.setPassportNumber("AB123456");
app.setNationality("US");
app.setVisaType("Tourist");
app.setDestinationCountry("FR");
app.setDurationDays(30);
app.setApplicationFee(50.0);
app.setProcessingFee(25.0);
// Save
facade.save();
// Verify
assertNotNull(app.getApplicationId());
assertEquals("John Doe", app.getApplicantName());
assertEquals(75.0, facade.calculateTotalCost(), 0.01);
}
@Test
public void testLoadVisaApplication() throws TlinqClientException {
String token = getTestToken();
Integer testId = 1;
// Load existing application
VisaApplicationFacade facade = new VisaApplicationFacade(token, testId);
CVisaApplication app = facade.getApplication();
// Verify
assertNotNull(app);
assertEquals(testId, app.getApplicationId());
assertNotNull(app.getApplicantName());
}
@Test
public void testSearchVisaApplications() throws TlinqClientException {
String token = getTestToken();
VisaApplicationFacade facade = new VisaApplicationFacade(token);
SelectCriteriaList criteria = new SelectCriteriaList();
criteria.add("status", "=", "pending");
List<CVisaApplication> results = facade.search(criteria);
assertNotNull(results);
assertTrue(results.size() > 0);
}
private String getTestToken() {
// Get test security token
return "test-token";
}
}
Step 9: Document the Entity¶
Add documentation to the entity mapping docs:
File: config/entities/docs/visa-mapping.md
Update or create the documentation file with your entity details.
Step 10: Build and Deploy¶
Advanced Topics¶
Multi-Factory Entities¶
Some entities can work with multiple service factories:
<Entity name="Product" class="com.perun.tlinq.entity.product.CProduct">
<EntityFactoryList>
<!-- Odoo backend -->
<Factory name="OdooServiceFactory" nativeEntity="...">
<!-- Odoo-specific services and mappings -->
</Factory>
<!-- Rayna B2B backend -->
<Factory name="RaynaB2BServiceFactory" nativeEntity="...">
<!-- Rayna-specific services and mappings -->
</Factory>
</EntityFactoryList>
</Entity>
Usage:
// Use default factory (Odoo)
ProductFacade facade = new ProductFacade(token);
facade.load(productId);
// Use specific factory
facade.setFactoryName("RaynaB2BServiceFactory");
facade.load(productId);
Custom Service Actions¶
Beyond CRUD, you can define custom actions:
<Service name="cancelBooking" action="cancel">
<NamedParams>
<Param name="bookingId" source="input"/>
<Param name="reason" source="input"/>
</NamedParams>
</Service>
Implementation:
public CBooking cancelBooking(String token, Integer bookingId, String reason) {
// Custom cancellation logic
}
Nested Entity Mappings¶
For complex relationships:
<FieldMapping targetField="customer"
targetFieldEntity="Customer"
sourceField="partner_id"
mapping="NestedMapping">
<ModelLookupMapping>
<ParentEntity name="Customer" idField="id"/>
</ModelLookupMapping>
</FieldMapping>
Method Reference Mappings¶
Use method calls for field transformation:
This calls getStatusString() method on the native entity.
Computed Fields¶
Fields calculated on the fly:
@Override
public void postLoad() {
super.postLoad();
// Compute total from line items
this.total = calculateTotal();
}
private Double calculateTotal() {
return items.stream()
.mapToDouble(Item::getAmount)
.sum();
}
Mark as read-only in config:
<FieldMapping targetField="total"
sourceField="amount_total"
mapping="DirectMapping"
reverseMapping="NoMapping"/>
Best Practices¶
Entity Design¶
- Single Responsibility: Each entity represents one business concept
- Immutable IDs: Never change ID fields after creation
- Null Safety: Check for nulls, especially in relationships
- Data Types: Use appropriate types (Integer for IDs, LocalDate for dates, Double for money)
- Naming: Follow naming conventions (C prefix, camelCase)
Field Naming¶
- Descriptive Names:
customerIdnotcid - Consistent Suffixes:
Idfor IDs,Namefor names,Datefor dates - Boolean Prefix:
isActive,hasPermission,canEdit - Arrays/Lists: Plural names
items,documents
Configuration¶
- Organize by Domain: Keep related entities in the same XML file
- Complete Mappings: Map all fields, even if NoMapping
- Document Unusual Mappings: Add XML comments for complex mappings
- Test Configuration: Validate XML after changes
Service Implementation¶
- Error Handling: Always throw
TlinqClientExceptionwith descriptive messages - Null Checks: Validate inputs before processing
- Logging: Log important operations and errors
- Transaction Safety: Ensure operations are atomic when possible
Facade Pattern¶
- One Facade per Entity: Don't mix entity types in one facade
- Stateful: Facades hold current entity state
- Business Logic: Put business rules in facade, not entity
- Simplified API: Hide complexity from calling code
Testing¶
- Unit Tests: Test entity creation, getters, setters
- Integration Tests: Test full CRUD lifecycle
- Mock Backends: Use mocks for external services
- Test Data: Use realistic test data
- Edge Cases: Test nulls, empty collections, boundary values
Examples¶
Example 1: Simple Entity (Read-Only Reference Data)¶
Use Case: Currency reference data from Odoo
// Entity
public class CCurrency extends TlinqEntity {
protected Integer id;
protected String name;
protected String symbol;
protected Double rate;
// Getters and setters...
}
<!-- Configuration -->
<Entity name="Currency"
class="com.perun.tlinq.entity.common.CCurrency"
idField="id"
defaultFactory="OdooServiceFactory">
<EntityFactoryList>
<Factory name="OdooServiceFactory" nativeEntity="...">
<ServiceList>
<Service name="getCurrencies" action="search"/>
</ServiceList>
<FieldMappingList>
<FieldMapping targetField="id" sourceField="id" mapping="DirectMapping"/>
<FieldMapping targetField="name" sourceField="name" mapping="DirectMapping"/>
<FieldMapping targetField="symbol" sourceField="symbol" mapping="DirectMapping"/>
<FieldMapping targetField="rate" sourceField="rate" mapping="DirectMapping"/>
</FieldMappingList>
</Factory>
</EntityFactoryList>
</Entity>
Example 2: Entity with Relationships¶
Use Case: Quote with line items
// Parent entity
public class CQuote extends TlinqEntity {
protected Integer quoteId;
protected String quoteName;
protected Integer customerId;
protected String customerName;
protected List<CQuoteItem> items;
protected Double total;
// Getters and setters...
}
// Child entity
public class CQuoteItem extends TlinqEntity {
protected Integer itemId;
protected Integer quoteId;
protected String description;
protected Integer quantity;
protected Double unitPrice;
protected Double amount;
// Getters and setters...
}
Example 3: Entity with Complex Mappings¶
Use Case: Flight offer with nested pricing
public class CFlightOffer extends TlinqEntity {
protected String offerId;
protected List<CItinerary> itineraries;
protected List<CTravelerPricing> travelerPricing;
protected CPrice price;
// Complex nested structure
}
<FieldMapping targetField="itineraries"
sourceField="itineraries"
mapping="ArrayMapping"/>
<FieldMapping targetField="price.total"
sourceField="price.grandTotal"
mapping="NestedMapping"/>
Example 4: Usage in Controller¶
@RestController
@RequestMapping("/api/visa")
public class VisaController {
@PostMapping("/applications")
public ResponseEntity<CVisaApplication> createApplication(
@RequestHeader("Authorization") String token,
@RequestBody VisaApplicationRequest request) {
try {
// Create facade
VisaApplicationFacade facade =
new VisaApplicationFacade(token, true);
// Populate from request
CVisaApplication app = facade.getApplication();
app.setApplicantName(request.getName());
app.setPassportNumber(request.getPassport());
app.setVisaType(request.getVisaType());
// ... more fields
// Save
facade.save();
return ResponseEntity.ok(app);
} catch (TlinqClientException e) {
return ResponseEntity.badRequest().build();
}
}
@GetMapping("/applications/{id}")
public ResponseEntity<CVisaApplication> getApplication(
@RequestHeader("Authorization") String token,
@PathVariable Integer id) {
try {
VisaApplicationFacade facade =
new VisaApplicationFacade(token, id);
return ResponseEntity.ok(facade.getApplication());
} catch (TlinqClientException e) {
return ResponseEntity.notFound().build();
}
}
@PostMapping("/applications/{id}/submit")
public ResponseEntity<CVisaApplication> submitApplication(
@RequestHeader("Authorization") String token,
@PathVariable Integer id) {
try {
VisaApplicationFacade facade =
new VisaApplicationFacade(token, id);
facade.submitForApproval();
return ResponseEntity.ok(facade.getApplication());
} catch (TlinqClientException e) {
return ResponseEntity.badRequest().build();
}
}
}
Troubleshooting¶
Common Issues¶
1. Entity Not Found¶
Error: Entity 'MyEntity' not found in configuration
Solutions:
- Verify entity name in XML matches code
- Check XInclude path in main config
- Rebuild project after config changes
- Check XML file is in entities/ directory
2. Field Mapping Errors¶
Error: Cannot map field 'fieldName'
Solutions: - Verify field exists in both TQPRO and native entity - Check field name spelling in XML - Verify mapping type is appropriate - Check data type compatibility
3. Service Not Found¶
Error: Service 'serviceName' not found for entity 'EntityName'
Solutions: - Add service to entity configuration - Check service name matches method name - Verify action type is correct - Implement service method in factory
4. Factory Not Available¶
Error: Factory 'FactoryName' not found
Solutions: - Check factory is registered in main config - Verify factory name spelling - Check factory is enabled - Verify factory implementation exists
5. Null Pointer Exceptions¶
Common Causes: - Accessing nested objects without null checks - Missing field mappings - Backend returning null values - Uninitialized collections
Solutions: - Add null checks before accessing objects - Initialize collections in constructor - Use Optional for nullable fields - Add defensive programming
Debug Tips¶
-
Enable Logging:
-
Inspect Configuration:
-
Test XML Configuration:
-
Verify Field Mapping:
Reference¶
Package Structure¶
com.perun.tlinq
├── entity/ # Entity classes
│ ├── AbstractEntity.java
│ ├── TlinqEntity.java
│ ├── customer/ # Customer domain
│ ├── product/ # Product domain
│ ├── flight/ # Flight domain
│ └── ...
├── config/ # Configuration classes
│ ├── EntityConfig.java
│ ├── EntityList.java
│ ├── FieldMappingConfig.java
│ └── ...
├── util/ # Utilities
│ ├── ClientConfig.java
│ ├── TlinqClientException.java
│ └── ...
└── client/ # Backend integrations
├── odoo/
├── amadeus/
└── ...
Key Classes¶
| Class | Purpose |
|---|---|
AbstractEntity |
Base entity with reflection utilities |
TlinqEntity |
Framework integration layer |
EntityFacade |
Base facade for entity operations |
ClientConfig |
Configuration loader and accessor |
EntityConfig |
Entity metadata and mappings |
TlinqEntityFactory |
Entity factory for CRUD operations |
ServiceFactoryConfig |
Service factory configuration |
Configuration Files¶
| File | Purpose |
|---|---|
config/tourlinq-config.xml |
Main configuration with XInclude references |
config/entities/*.xml |
Domain-specific entity configurations |
config/entities/docs/*.md |
Entity mapping documentation |
Annotations¶
| Annotation | Purpose | Usage |
|---|---|---|
@TlinqEntityField |
Mark entity fields | @TlinqEntityField(sourceName="native_field") |
@Transient |
Exclude from persistence | @Transient private String temp; |
Configuration Reference¶
See the entity mapping documentation in config/entities/docs/ for detailed field mappings and service configurations for each domain.
Conclusion¶
This guide provides a comprehensive overview of entity management in TQPRO. By following these guidelines and best practices, you can successfully create, configure, and maintain entities within the framework.
Quick Checklist for New Entity¶
- [ ] Define business requirements
- [ ] Create entity class extending
TlinqEntity - [ ] Add to appropriate domain package
- [ ] Create entity configuration XML
- [ ] Add field mappings
- [ ] Define services (CRUD + custom)
- [ ] Create native entity class
- [ ] Implement service methods in factory
- [ ] Create facade class (recommended)
- [ ] Write tests
- [ ] Document in mapping docs
- [ ] Build and deploy
Further Reading¶
- Entity Mapping Documentation:
config/entities/docs/README.md - Service Factory Guide:
docs/SERVICE_FACTORY_GUIDE.md - API Documentation:
docs/API_REFERENCE.md - TQPRO Architecture:
docs/ARCHITECTURE.md
Document Version: 1.0 Last Updated: 2025-11-19 Maintained by: TourLinq Development Team
For questions or issues, please contact the development team or file an issue in the project repository.