
5 Property Models
Properties are a fundamental mechanism in the impulse framework for configuring functional blocks such as serializers, signal processors, producers, and other components. The property system provides a consistent way to define, validate, and access configuration parameters while enabling user-friendly interfaces for configuration.
This guide explores the property system in detail, focusing on creating and using property models while also covering related aspects such as validation, typed properties, and user interface integration.
Understanding Properties in impulse
Properties in the impulse framework serve as the primary mechanism for making components configurable and adaptable to different usage scenarios. Whether you're reading data from files, processing signals, or producing visualizations, properties provide the flexibility needed to customize behavior without modifying code.
Property System Overview
At the core of the impulse property system is the IPropertyModel
interface, which represents a collection of named properties with associated metadata. Each property has:
- A unique identifier (key)
- A current value
- A default value
- An optional type (beyond string)
- Optional validation rules
- Metadata like descriptive labels and help text
- Optional predefined choices (options)
Property models are used throughout the framework to:
- Configure components: Adjust the behavior of serializers, processors, producers, etc.
- Store user preferences: Maintain user-specific settings
- Define extension parameters: Allow extensions to expose configurable options
- Validate inputs: Ensure values meet requirements before they're applied
- Generate user interfaces: Automatically create configuration dialogs and forms
Key Concepts
Before diving into implementation details, it's important to understand these core concepts:
Property Types: While all properties can be represented as strings for persistence, the property system supports typed properties including booleans, integers, doubles, enumerations, and custom types.
Validation: Properties can have validation rules to ensure they contain valid values. Validation can range from simple type checking to complex domain-specific rules.
Options: Properties can define a set of predefined choices, displayed as dropdown menus or radio buttons in UIs.
Default Values: Every property has a default value that can be used to reset configuration or as a starting point.
Control Providers: The property system can be extended with custom UI control providers for specialized editing experiences.
The IPropertyModel Interface
The IPropertyModel
interface is the foundation of the property system, defining methods for property management, access, and validation.
Core Capabilities
The interface provides methods for:
// Get or set string values String getVal(String id); void setVal(String id, String value); // Access typed values Object getTyped(String id);T getTyped(String id, Class cs); void setTyped(String id, Object value); // Manage defaults String getDefault(String id); void reset(String id); boolean isDefault(String id); // Work with options String[] getOptions(String id); // Get property metadata Class> getType(String id); boolean isEnabled(String id);
Creating Property Models
There are several approaches to creating property models, depending on your needs and the complexity of your component.
Using the Factory Pattern
The most common approach is to use the factory methods, which provides a fluent API for defining properties:
// Create a basic property model PropertyModel properties = new PropertyModel() // String property with validation .add("filename", "data.log", null, null, "File Name", null, "The input file to process") // Boolean property .add("verbose", true, null, "Verbose Output", null, "Enable detailed logging") // Integer property with options .add("mode", 0, new String[]{"Fast", "Normal", "Accurate"}, new Object[]{0, 1, 2}, null, "Processing Mode", null, "Select processing accuracy/speed tradeoff") // Double property with range validation .add("threshold", 0.5, null, null, "Threshold", null, "Detection threshold (0.0-1.0)");
Method Descriptions:
Method | Description | Parameters | Return Value | Notes |
---|---|---|---|---|
PropertyModel() |
Creates a new empty property model | None | PropertyModel |
Starting point for building a property collection |
add(String, Object, ...) |
Adds a property with specified characteristics | Various overloads including ID, default value, validation, etc. | PropertyModel (for chaining) |
Core method for defining properties |
add(String, String, ...) |
Adds a string property | String ID, default value, and metadata | PropertyModel (for chaining) |
String-specific version of add |
add(String, boolean, ...) |
Adds a boolean property | String ID, default value, and metadata | PropertyModel (for chaining) |
Boolean-specific version of add |
add(String, int, ...) |
Adds an integer property | String ID, default value, and metadata | PropertyModel (for chaining) |
Integer-specific version of add |
add(String, double, ...) |
Adds a double property | String ID, default value, and metadata | PropertyModel (for chaining) |
Double-specific version of add |
The add()
method has several overloads to support different property types and configurations. The parameters typically include:
- Property key (identifier)
- Default value
- Option labels (for dropdown lists)
- Option values (corresponding to labels)
- Validation
- Information (label, icon, description, help URL)
Conditional Property Creation
The factory also supports conditional property creation using addIf()
, which is useful for creating properties only when certain conditions are met:
// Only add this property if advanced features are enabled boolean advancedMode = true; properties.addIf(advancedMode, "optimizationLevel", 2, new String[]{"Basic", "Advanced", "Experimental"}, new Object[]{1, 2, 3}, null, "Optimization Level", null, "Algorithm optimization level");
Method Descriptions:
Method | Description | Parameters | Return Value | Notes |
---|---|---|---|---|
addIf(boolean, String, Object, ...) |
Conditionally adds a property | Condition flag followed by normal add parameters | PropertyModel (for chaining) |
Property is only added if condition is true |
addIf(boolean, String, String, ...) |
Conditionally adds a string property | Condition flag followed by string property parameters | PropertyModel (for chaining) |
String-specific conditional version |
addIf(boolean, String, boolean, ...) |
Conditionally adds a boolean property | Condition flag followed by boolean property parameters | PropertyModel (for chaining) |
Boolean-specific conditional version |
addIf(boolean, String, int, ...) |
Conditionally adds an integer property | Condition flag followed by integer property parameters | PropertyModel (for chaining) |
Integer-specific conditional version |
addIf(boolean, String, double, ...) |
Conditionally adds a double property | Condition flag followed by double property parameters | PropertyModel (for chaining) |
Double-specific conditional version |
Working with Typed Properties
While all properties can be accessed as strings, the typed property system provides type safety and convenience when working with non-string values.
Defining Typed Properties
When creating a property, you can specify its type:
// Integer property properties.add("count", 100, null, null, "Count", null, "Number of items to process"); // Double property properties.add("scale", 1.5, null, null, "Scale Factor", null, "Scaling factor for values"); // Boolean property properties.add("enabled", true, null, "Enabled", null, "Enable this feature");
Accessing Typed Values
Typed properties can be accessed using type-specific methods:
// Get typed values int count = properties.getTyped("count", Integer.class); double scale = properties.getTyped("scale", Double.class); boolean enabled = properties.getTyped("enabled", Boolean.class); // Set typed values properties.setTyped("count", 200); properties.setTyped("scale", 2.0); properties.setTyped("enabled", false);
Property Validation
Validation ensures that property values meet specific requirements before they're applied, preventing invalid configurations.
Built-in Validation Results
The impulse framework provides standard validation results in the ValidationResult
class:
// Standard validation results ValidationResult.OK // Validation passed ValidationResult.UNCHANGED // Validation passed, value unchanged ValidationResult.UNKNOWN_ERROR // Generic error result ValidationResult.NUMBER_FORMAT_ERROR // Invalid number format ValidationResult.BOOLEAN_FORMAT_ERROR // Invalid boolean format
Method Descriptions:
Constant | Description | Value | Use Case |
---|---|---|---|
ValidationResult.OK |
Indicates successful validation | IValidationResult.STATUS_OK |
Return when value passes all validation rules |
ValidationResult.UNCHANGED |
Indicates value is valid but unchanged | IValidationResult.STATUS_UNCHANGED |
Return when validation doesn't modify the value |
ValidationResult.UNKNOWN_ERROR |
Generic error for unspecified issues | IValidationResult.STATUS_ERROR |
Default error when specific error not available |
ValidationResult.NUMBER_FORMAT_ERROR |
Error for invalid numeric formats | IValidationResult.STATUS_ERROR |
When string can't be parsed as a number |
ValidationResult.BOOLEAN_FORMAT_ERROR |
Error for invalid boolean formats | IValidationResult.STATUS_ERROR |
When string can't be parsed as boolean |
Inline Validation with Lambda Expressions
For simple validation scenarios, you can use lambda expressions directly within the add()
method, which provides a concise way to define validation logic:
// Using lambda for required field validation properties.add("name", "", (value, flags) -> { if (value == null || value.trim().isEmpty()) { return new ValidationResult(IValidationResult.STATUS_ERROR, "Name is required"); } return ValidationResult.OK; }, "Name", null, "User's name");
Method Descriptions:
Method/Parameter | Description | Parameters | Return Value | Notes |
---|---|---|---|---|
add() with validator |
Adds property with inline validation | Various including lambda validator | PropertyModel (for chaining) |
Fluent API for adding validated properties |
Lambda validator | Anonymous function for validation | value - The value to validate, flags - Validation context flags |
IValidationResult - Validation outcome |
Compact syntax for simple validation rules |
ValidationResult constructor |
Creates validation result | status - Result status code, message - Error message |
ValidationResult |
Used to return custom validation results |
This approach is particularly useful for:
- Simple validation rules that don't need to be reused
- Quick prototyping
- Self-contained components with specialized validation
- Reducing boilerplate code for simple validation cases
For more complex validation or rules that need to be shared across multiple properties, the full validator implementation approach shown in the next section provides better organization and reusability.
Implementing Validators
To create validators, implement the IValidation.Single
interface:
// Basic required value validator IValidation.SinglerequiredValidator = new IValidation.Single () { @Override public IValidationResult validate(String value, int flags) { if (value == null || value.trim().isEmpty()) { return new ValidationResult(IValidationResult.STATUS_ERROR, "Value is required"); } return ValidationResult.OK; } }; // Range validator for numeric values IValidation.Single rangeValidator = new IValidation.Single () { private final double min = 0; private final double max = 100; @Override public IValidationResult validate(String value, int flags) { try { double numValue = Double.parseDouble(value); if (numValue < min || numValue > max) { return new ValidationResult(IValidationResult.STATUS_ERROR, "Value must be between " + min + " and " + max); } return ValidationResult.OK; } catch (NumberFormatException e) { return ValidationResult.NUMBER_FORMAT_ERROR; } } }; // Pattern validator for string format IValidation.Single patternValidator = new IValidation.Single () { private final String pattern = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"; @Override public IValidationResult validate(String value, int flags) { if (value != null && !value.matches(pattern)) { return new ValidationResult(IValidationResult.STATUS_ERROR, "Value must match pattern: " + pattern); } return ValidationResult.OK; } };
Properties with Options
Properties with predefined choices provide a better user experience by limiting selection to valid values and making configuration more intuitive.
Defining Options
Options are defined as parallel arrays of labels (displayed to users) and values (used internally):
// Simple options (string values same as labels) properties.add("logLevel", "INFO", new String[]{"DEBUG", "INFO", "WARN", "ERROR"}, null, "Log Level", null, "Logging verbosity"); // Options with different internal values properties.add("quality", 1, new String[]{"Low", "Medium", "High", "Ultra"}, new Object[]{0, 1, 2, 3}, null, "Quality", null, "Rendering quality");
Method Descriptions:
Method | Description | Parameters | Return Value | Notes |
---|---|---|---|---|
add() with options |
Adds property with predefined choices | String - Property ID, Object - Default value, String[] - Option labels, Object[] - Option values, ... |
PropertyModel (for chaining) |
Labels shown to users; Values stored internally |
getOptions(String) |
Gets available options for a property | String - Property ID |
String[] - Array of option labels |
For displaying available choices in UI |
getTyped(String) |
Gets the internal value of selected option | String - Property ID |
Object - Selected option's value |
Returns the value object, not the label |
getVal(String) |
Gets the label of selected option | String - Property ID |
String - Selected option's label |
Returns the display text of the selection |
Property UI Integration
The property system integrates with UI frameworks to automatically generate configuration interfaces.
Control Providers
Control providers customize how properties are presented in the UI.
The control provider class implements ITlkControlProvider
interface to create specialized UI components for properties.
Information Metadata
Property metadata helps create better user interfaces:
properties.add("timeout", 30, null, null, "Connection Timeout", "clock-icon", // Label and icon "Maximum time to wait for connection in seconds", // Description "https://docs.example.com/timeout"); // Help URL
This metadata is used by UIs to display:
- Proper labels
- Icons
- Tooltip descriptions
- Help links
Note: This section will be expanded in the future with more detailed information about UI integration options, custom control providers, and advanced layout techniques.
Property Models in Functional Blocks
Functional blocks in the impulse framework, such as serializers, record readers, signal processors, and producers, use property models to define their configuration interfaces. The framework provides specialized factory methods for creating property models tailored to each type of functional block.
Defining Standard Property Models
When implementing a functional block, you typically define a static getPropertyModel()
method that creates a property model with standard properties for your component type, plus any custom properties specific to your implementation:
static public IPropertyModel getPropertyModel(ISerializerDescriptor object, Object context) { boolean notPref = context != IRegistryObject.Preference.class; // Start with standard properties for this type of component return IParsingRecordReader.getPropertyModel(PROP_INLCLUDE) // Add component-specific properties .add("blockSize", 1024, null, null, "Block Size", null, "Size of data blocks to process at once") // Add standard console logging properties .add(ConfiguredConsoleStream.getPropertyModel()); }
This approach ensures that:
- Your component inherits all standard properties required for its functional category
- You can add custom properties specific to your implementation
- You can include standard property groups from other subsystems (like logging)
- The property model is consistent with user expectations for that component type
Accessing Properties in Functional Blocks
Functional blocks in the impulse framework provide consistent helper methods for accessing property values in implementation code. These methods simplify property access and make your code more readable by eliminating the need to directly interact with the property model in most cases.
// Get a typed property with specific type int blockSize = getTypedProperty("blockSize", Integer.class); double threshold = getTypedProperty("threshold", Double.class); boolean enabled = getTypedProperty("enabled", Boolean.class); // Get a property as generic Object Object value = getTypedProperty("complexProperty"); // Get a property as String String filename = getProperty("filename"); // Check if a property is set to its default value boolean isUsingDefault = isPropertyDefault("blockSize");
These helper methods automatically handle the common tasks of locating the property model and performing null-safety checks, allowing you to focus on the business logic of your component rather than property access mechanics.
Method Descriptions:
Method | Description | Parameters | Return Value | Notes |
---|---|---|---|---|
getTypedProperty(String, Class |
Gets a property value with type safety | String - Property key, Class - Expected type |
T - Typed property value |
Returns null if property or model doesn't exist |
getTypedProperty(String) |
Gets a property value as Object | String - Property key |
Object - Property value |
Returns null if property or model doesn't exist |
getProperty(String) |
Gets a property value as String | String - Property key |
String - String value |
Returns null if property or model doesn't exist |
isPropertyDefault(String) |
Checks if property is at default value | String - Property key |
boolean - true if default |
Returns true if property or model doesn't exist |
Best Practices for Property Access
When working with properties in functional blocks:
- Type Safety: Always use the typed accessor methods when you know the expected type.
- Default Values: Design your component to work with default values by setting reasonable defaults in your property model.
- Validation: Don't assume properties contain valid values in your access code; the property system's validation should handle this.
- Performance: Cache property values that are used frequently instead of looking them up repeatedly.
- Error Handling: Include graceful fallbacks for missing or invalid properties.
Conclusion
The property system in the impulse framework provides a flexible and powerful way to configure components, validate inputs, and generate user interfaces. By understanding how to create and manage property models, you can develop customizable, user-friendly components that integrate seamlessly with the rest of the framework.