Property Extractors
Property extractors define custom property access logic for objects that don't follow standard JavaBean patterns or require specialized property extraction. They provide a flexible way to access properties from any object structure.
Overview
Property extractors are used when:
- Objects don't follow JavaBean getter conventions
- You need to compute properties dynamically
- You want to provide custom property names or aliases
- Objects use non-standard property access methods
- You need to access properties from legacy systems
Custom Property Extractors
Define custom property access logic by implementing the PropertyExtractor interface:
Basic Property Extractor
// Database entity extractor
PropertyExtractor entityExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof DatabaseEntity;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
DatabaseEntity entity = (DatabaseEntity) obj;
switch (prop) {
case "id": return entity.getPrimaryKey();
case "displayName": return entity.computeDisplayName();
default: return entity.getAttribute(prop);
}
}
};
// Registration
var converter = BasicBeanConverter.builder()
.defaultSettings()
.addPropertyExtractor(entityExtractor)
.build();
Advanced Property Extractor Example
// Map-based property extractor with type conversion
PropertyExtractor mapExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof Map;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
Map<?, ?> map = (Map<?, ?>) obj;
// Support special properties
if ("size".equals(prop)) {
return map.size();
}
if ("isEmpty".equals(prop)) {
return map.isEmpty();
}
if ("keys".equals(prop)) {
return new ArrayList<>(map.keySet());
}
if ("values".equals(prop)) {
return new ArrayList<>(map.values());
}
// Standard key access
return map.get(prop);
}
};
Property Alias Extractor
// Extractor that provides property aliases
PropertyExtractor aliasExtractor = new PropertyExtractor() {
private final Map<String, String> aliases = Map.of(
"fname", "firstName",
"lname", "lastName",
"email", "emailAddress",
"phone", "phoneNumber"
);
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return aliases.containsKey(prop);
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
String actualProp = aliases.get(prop);
try {
String methodName = "get" + capitalize(actualProp);
Method method = obj.getClass().getMethod(methodName);
return method.invoke(obj);
} catch (Exception e) {
throw new RuntimeException("Failed to extract property: " + prop, e);
}
}
private String capitalize(String s) {
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
};
// Usage
assertBean(args().setBeanConverter(converter),
user, "fname,lname,email", "John,Doe,john@example.com");
Computed Property Extractor
// Extractor for computed/derived properties
PropertyExtractor computedExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof User && prop.startsWith("computed_");
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
User user = (User) obj;
switch (prop) {
case "computed_fullName":
return user.getFirstName() + " " + user.getLastName();
case "computed_age":
return Period.between(user.getBirthDate(), LocalDate.now()).getYears();
case "computed_initials":
return user.getFirstName().charAt(0) + "." + user.getLastName().charAt(0) + ".";
default:
throw new IllegalArgumentException("Unknown computed property: " + prop);
}
}
};
// Usage
assertBean(args().setBeanConverter(converter),
user, "computed_fullName,computed_age,computed_initials",
"John Doe,30,J.D.");
Complex Property Extraction Examples
XML/JSON Document Extractor
// Extract properties from XML/JSON documents
PropertyExtractor documentExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof JsonNode || obj instanceof XmlNode;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
if (obj instanceof JsonNode) {
JsonNode node = (JsonNode) obj;
return node.get(prop);
} else if (obj instanceof XmlNode) {
XmlNode node = (XmlNode) obj;
return node.getAttribute(prop);
}
return null;
}
};
Reflection-Based Private Field Extractor
// Access private fields via reflection
PropertyExtractor privateFieldExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return prop.startsWith("_"); // Private fields prefixed with underscore
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
try {
String fieldName = prop.substring(1); // Remove underscore prefix
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
throw new RuntimeException("Failed to access field: " + prop, e);
}
}
};
// Usage
assertBean(args().setBeanConverter(converter),
myBean, "_privateField1,_privateField2", "value1,value2");
SQL ResultSet Extractor
// Extract columns from ResultSet
PropertyExtractor resultSetExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof ResultSet;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
ResultSet rs = (ResultSet) obj;
try {
// Support column index access
if (prop.matches("\\d+")) {
int index = Integer.parseInt(prop);
return rs.getObject(index);
}
// Support column name access
return rs.getObject(prop);
} catch (SQLException e) {
throw new RuntimeException("Failed to extract column: " + prop, e);
}
}
};
Configuration Property Extractor
// Extract properties from configuration objects
PropertyExtractor configExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof Configuration;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
Configuration config = (Configuration) obj;
// Support dot-notation for nested properties
if (prop.contains(".")) {
String[] parts = prop.split("\\.");
Object current = config;
for (String part : parts) {
current = ((Configuration) current).getProperty(part);
if (current == null) return null;
}
return current;
}
return config.getProperty(prop);
}
};
// Usage
assertBean(args().setBeanConverter(converter),
config, "database.host,database.port,app.name",
"localhost,5432,MyApp");
Best Practices
When to Create Custom Property Extractors
- Non-standard property access patterns
- Legacy systems without JavaBean getters
- Computed or derived properties
- Property aliases or shortcuts
- Document/tree structures (XML, JSON)
- Database results or cursors
- Dynamic property systems
Property Extractor Guidelines
- Always implement
canExtract()to check object compatibility - Handle null objects and properties gracefully
- Throw descriptive exceptions for invalid properties
- Document property naming conventions
- Consider performance for frequently accessed properties
- Keep extraction logic simple and focused
- Avoid side effects or state modifications
Performance Considerations
// Cache expensive computations
PropertyExtractor cachedExtractor = new PropertyExtractor() {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof ExpensiveObject;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
String cacheKey = System.identityHashCode(obj) + ":" + prop;
return cache.computeIfAbsent(cacheKey, k -> {
// Expensive computation here
return ((ExpensiveObject) obj).computeProperty(prop);
});
}
};
Error Handling
// Robust error handling in property extractors
PropertyExtractor safeExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof MyObject;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
try {
MyObject myObj = (MyObject) obj;
return myObj.getProperty(prop);
} catch (PropertyNotFoundException e) {
return "<not found>";
} catch (Exception e) {
return "<error: " + e.getClass().getSimpleName() + ">";
}
}
};
Thread Safety
// Ensure thread-safe property extraction
PropertyExtractor threadSafeExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return obj instanceof ThreadSafeObject;
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
ThreadSafeObject tso = (ThreadSafeObject) obj;
synchronized (tso) {
return tso.getProperty(prop);
}
}
};
Common Patterns
Chain of Responsibility Pattern
// Multiple extractors chained together
PropertyExtractor chainedExtractor = new PropertyExtractor() {
private final List<PropertyExtractor> extractors = Arrays.asList(
entityExtractor,
mapExtractor,
computedExtractor
);
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return extractors.stream()
.anyMatch(e -> e.canExtract(conv, obj, prop));
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
for (PropertyExtractor extractor : extractors) {
if (extractor.canExtract(conv, obj, prop)) {
return extractor.extract(conv, obj, prop);
}
}
throw new IllegalArgumentException("No extractor found for property: " + prop);
}
};
Fallback Extractor
// Try multiple extraction strategies with fallback
PropertyExtractor fallbackExtractor = new PropertyExtractor() {
@Override
public boolean canExtract(BeanConverter conv, Object obj, String prop) {
return true; // Universal extractor
}
@Override
public Object extract(BeanConverter conv, Object obj, String prop) {
// Try getter method
try {
String methodName = "get" + capitalize(prop);
Method method = obj.getClass().getMethod(methodName);
return method.invoke(obj);
} catch (Exception e1) {
// Try field access
try {
Field field = obj.getClass().getDeclaredField(prop);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e2) {
// Try map access
if (obj instanceof Map) {
return ((Map<?, ?>) obj).get(prop);
}
return null;
}
}
}
};
Usage Examples
Testing Custom Objects
// Test database entity
DatabaseEntity entity = loadEntity(123);
assertBean(args().setBeanConverter(converter),
entity, "id,displayName,createdDate", "123,John Doe,2023-01-15");
// Test configuration
Configuration config = loadConfig();
assertBean(args().setBeanConverter(converter),
config, "database.host,database.port,app.timeout",
"localhost,5432,30000");
// Test computed properties
User user = loadUser(456);
assertBean(args().setBeanConverter(converter),
user, "computed_fullName,computed_age", "Alice Smith,28");
Combining with Other Features
// Use property extractor with nested access
assertBean(args().setBeanConverter(converter),
order, "customer{computed_fullName},items{0{name}}",
"{John Doe},{{Laptop}}");
See Also
- Stringifiers - Converting objects to strings
- Listifiers - Converting collection-like objects to lists
- Swappers - Transforming objects before processing
- juneau-bct Basics - Main BCT documentation
Share feedback or follow-up questions for this page directly through GitHub.