Skip to main content

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

Discussion

Share feedback or follow-up questions for this page directly through GitHub.