CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework--spring-expression

Spring Expression Language (SpEL) provides a powerful expression language for querying and manipulating object graphs at runtime.

Pending
Overview
Eval results
Files

property-index-access.mddocs/

Property and Index Access

This document covers SpEL's property and index access capabilities, including accessor interfaces, standard implementations, and custom accessor development.

Core Accessor Interfaces

TargetedAccessor Interface

public interface TargetedAccessor {
    Class<?>[] getSpecificTargetClasses();
}

{ .api }

The base interface for accessors that target specific classes for optimization.

PropertyAccessor Interface

public interface PropertyAccessor extends TargetedAccessor {
    boolean canRead(EvaluationContext context, Object target, String name) throws AccessException;
    TypedValue read(EvaluationContext context, Object target, String name) throws AccessException;
    boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException;
    void write(EvaluationContext context, Object target, String name, Object newValue) 
        throws AccessException;
}

{ .api }

IndexAccessor Interface

public interface IndexAccessor extends TargetedAccessor {
    boolean canRead(EvaluationContext context, Object target, Object index) throws AccessException;
    TypedValue read(EvaluationContext context, Object target, Object index) throws AccessException;
    boolean canWrite(EvaluationContext context, Object target, Object index) throws AccessException;
    void write(EvaluationContext context, Object target, Object index, Object newValue) 
        throws AccessException;
}

{ .api }

Standard Property Accessors

ReflectivePropertyAccessor Class

public class ReflectivePropertyAccessor implements PropertyAccessor {
    public ReflectivePropertyAccessor();
    public ReflectivePropertyAccessor(boolean allowWrite);
    
    @Override
    public Class<?>[] getSpecificTargetClasses();
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, String name) 
        throws AccessException;
    @Override
    public TypedValue read(EvaluationContext context, Object target, String name) 
        throws AccessException;
    @Override
    public boolean canWrite(EvaluationContext context, Object target, String name) 
        throws AccessException;
    @Override
    public void write(EvaluationContext context, Object target, String name, Object newValue) 
        throws AccessException;
}

{ .api }

The default property accessor that uses Java reflection to access object properties via getter/setter methods and fields.

DataBindingPropertyAccessor Class

public class DataBindingPropertyAccessor implements PropertyAccessor {
    @Override
    public Class<?>[] getSpecificTargetClasses();
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, String name) 
        throws AccessException;
    @Override
    public TypedValue read(EvaluationContext context, Object target, String name) 
        throws AccessException;
    @Override
    public boolean canWrite(EvaluationContext context, Object target, String name) 
        throws AccessException;
    @Override
    public void write(EvaluationContext context, Object target, String name, Object newValue) 
        throws AccessException;
}

{ .api }

Optimized property accessor for data-binding scenarios with better performance characteristics.

Property Access Examples

public class Person {
    private String name;
    private int age;
    private boolean active;
    
    // Standard getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public boolean isActive() { return active; }
    public void setActive(boolean active) { this.active = active; }
}

// Using ReflectivePropertyAccessor
StandardEvaluationContext context = new StandardEvaluationContext();
context.addPropertyAccessor(new ReflectivePropertyAccessor());

Person person = new Person();
person.setName("John");
person.setAge(30);
person.setActive(true);

ExpressionParser parser = new SpelExpressionParser();

// Reading properties
Expression exp = parser.parseExpression("name");
String name = exp.getValue(context, person, String.class); // "John"

exp = parser.parseExpression("age");
Integer age = exp.getValue(context, person, Integer.class); // 30

exp = parser.parseExpression("active");
Boolean active = exp.getValue(context, person, Boolean.class); // true

// Writing properties
exp = parser.parseExpression("name");
exp.setValue(context, person, "Jane");
// person.getName() returns "Jane"

exp = parser.parseExpression("age");
exp.setValue(context, person, 25);
// person.getAge() returns 25

{ .api }

Standard Index Accessors

ReflectiveIndexAccessor Class

public class ReflectiveIndexAccessor implements CompilableIndexAccessor {
    @Override
    public Class<?>[] getSpecificTargetClasses();
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, Object index) 
        throws AccessException;
    @Override
    public TypedValue read(EvaluationContext context, Object target, Object index) 
        throws AccessException;
    @Override
    public boolean canWrite(EvaluationContext context, Object target, Object index) 
        throws AccessException;
    @Override
    public void write(EvaluationContext context, Object target, Object index, Object newValue) 
        throws AccessException;
    
    // CompilableIndexAccessor methods for compilation support
    public boolean isCompilable();
    public String generateCode(String indexedObjectName, String index, CodeFlow cf);
}

{ .api }

Index Access Examples

// Array access
int[] numbers = {1, 2, 3, 4, 5};
StandardEvaluationContext context = new StandardEvaluationContext();
context.addIndexAccessor(new ReflectiveIndexAccessor());
context.setVariable("numbers", numbers);

ExpressionParser parser = new SpelExpressionParser();

// Reading array elements
Expression exp = parser.parseExpression("#numbers[0]");
Integer first = exp.getValue(context, Integer.class); // 1

exp = parser.parseExpression("#numbers[2]");
Integer third = exp.getValue(context, Integer.class); // 3

// Writing array elements
exp = parser.parseExpression("#numbers[0]");
exp.setValue(context, 10);
// numbers[0] is now 10

// List access
List<String> names = new ArrayList<>(Arrays.asList("John", "Jane", "Bob"));
context.setVariable("names", names);

exp = parser.parseExpression("#names[1]");
String name = exp.getValue(context, String.class); // "Jane"

exp = parser.parseExpression("#names[1]");
exp.setValue(context, "Janet");
// names.get(1) returns "Janet"

// Map access
Map<String, Integer> ages = new HashMap<>();
ages.put("John", 30);
ages.put("Jane", 25);
context.setVariable("ages", ages);

exp = parser.parseExpression("#ages['John']");
Integer johnAge = exp.getValue(context, Integer.class); // 30

exp = parser.parseExpression("#ages['Jane']");
exp.setValue(context, 26);
// ages.get("Jane") returns 26

// String indexing (read-only)
String text = "Hello";
context.setVariable("text", text);

exp = parser.parseExpression("#text[1]");
String character = exp.getValue(context, String.class); // "e"

{ .api }

Compilation Support

CompilablePropertyAccessor Interface

public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes {
    boolean isCompilable();
    String generateCode(String propertyName, String target, CodeFlow cf);
}

{ .api }

CompilableIndexAccessor Interface

public interface CompilableIndexAccessor extends IndexAccessor, Opcodes {
    boolean isCompilable();
    String generateCode(String indexedObjectName, String index, CodeFlow cf);
}

{ .api }

Custom Property Accessors

Basic Custom Property Accessor

public class CustomObjectPropertyAccessor implements PropertyAccessor {
    
    @Override
    public Class<?>[] getSpecificTargetClasses() {
        return new Class<?>[] { CustomObject.class };
    }
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, String name) 
        throws AccessException {
        return target instanceof CustomObject && 
               ((CustomObject) target).hasProperty(name);
    }
    
    @Override
    public TypedValue read(EvaluationContext context, Object target, String name) 
        throws AccessException {
        if (target instanceof CustomObject) {
            CustomObject obj = (CustomObject) target;
            Object value = obj.getProperty(name);
            return new TypedValue(value);
        }
        throw new AccessException("Cannot read property: " + name);
    }
    
    @Override
    public boolean canWrite(EvaluationContext context, Object target, String name) 
        throws AccessException {
        return target instanceof CustomObject && 
               ((CustomObject) target).isPropertyWritable(name);
    }
    
    @Override
    public void write(EvaluationContext context, Object target, String name, Object newValue) 
        throws AccessException {
        if (target instanceof CustomObject) {
            CustomObject obj = (CustomObject) target;
            obj.setProperty(name, newValue);
        } else {
            throw new AccessException("Cannot write property: " + name);
        }
    }
}

// Custom object implementation
public class CustomObject {
    private final Map<String, Object> properties = new HashMap<>();
    private final Set<String> readOnlyProperties = new HashSet<>();
    
    public boolean hasProperty(String name) {
        return properties.containsKey(name);
    }
    
    public Object getProperty(String name) {
        return properties.get(name);
    }
    
    public void setProperty(String name, Object value) {
        if (!readOnlyProperties.contains(name)) {
            properties.put(name, value);
        }
    }
    
    public boolean isPropertyWritable(String name) {
        return !readOnlyProperties.contains(name);
    }
    
    public void markReadOnly(String name) {
        readOnlyProperties.add(name);
    }
}

// Usage
CustomObject obj = new CustomObject();
obj.setProperty("name", "John");
obj.setProperty("status", "active");
obj.markReadOnly("status");

StandardEvaluationContext context = new StandardEvaluationContext(obj);
context.addPropertyAccessor(new CustomObjectPropertyAccessor());

ExpressionParser parser = new SpelExpressionParser();

// Reading custom properties
Expression exp = parser.parseExpression("name");
String name = exp.getValue(context, String.class); // "John"

// Writing to writable property
exp = parser.parseExpression("name");
exp.setValue(context, "Jane");

// Attempting to write to read-only property (will be ignored)
exp = parser.parseExpression("status");
exp.setValue(context, "inactive"); // No change due to read-only

{ .api }

Map-based Property Accessor

public class MapAccessor implements PropertyAccessor {
    
    @Override
    public Class<?>[] getSpecificTargetClasses() {
        return new Class<?>[] { Map.class };
    }
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, String name) 
        throws AccessException {
        return target instanceof Map;
    }
    
    @Override
    public TypedValue read(EvaluationContext context, Object target, String name) 
        throws AccessException {
        if (target instanceof Map) {
            Map<?, ?> map = (Map<?, ?>) target;
            Object value = map.get(name);
            return new TypedValue(value);
        }
        throw new AccessException("Cannot read from non-Map object");
    }
    
    @Override
    public boolean canWrite(EvaluationContext context, Object target, String name) 
        throws AccessException {
        return target instanceof Map;
    }
    
    @Override
    @SuppressWarnings("unchecked")
    public void write(EvaluationContext context, Object target, String name, Object newValue) 
        throws AccessException {
        if (target instanceof Map) {
            Map<String, Object> map = (Map<String, Object>) target;
            map.put(name, newValue);
        } else {
            throw new AccessException("Cannot write to non-Map object");
        }
    }
}

// Usage example
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("user", "john");
dataMap.put("count", 42);

StandardEvaluationContext context = new StandardEvaluationContext(dataMap);
context.addPropertyAccessor(new MapAccessor());

ExpressionParser parser = new SpelExpressionParser();

// Access map values as properties
Expression exp = parser.parseExpression("user");
String user = exp.getValue(context, String.class); // "john"

exp = parser.parseExpression("count");
Integer count = exp.getValue(context, Integer.class); // 42

// Set new values
exp = parser.parseExpression("status");
exp.setValue(context, "active");
// dataMap now contains "status" -> "active"

{ .api }

Custom Index Accessors

Range-based Index Accessor

public class RangeIndexAccessor implements IndexAccessor {
    
    @Override
    public Class<?>[] getSpecificTargetClasses() {
        return new Class<?>[] { List.class, String.class };
    }
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, Object index) 
        throws AccessException {
        return (target instanceof List || target instanceof String) && 
               index instanceof Range;
    }
    
    @Override
    public TypedValue read(EvaluationContext context, Object target, Object index) 
        throws AccessException {
        if (index instanceof Range) {
            Range range = (Range) index;
            
            if (target instanceof List) {
                List<?> list = (List<?>) target;
                List<Object> result = new ArrayList<>();
                for (int i = range.getStart(); i <= range.getEnd() && i < list.size(); i++) {
                    result.add(list.get(i));
                }
                return new TypedValue(result);
            } else if (target instanceof String) {
                String str = (String) target;
                int start = Math.max(0, range.getStart());
                int end = Math.min(str.length(), range.getEnd() + 1);
                return new TypedValue(str.substring(start, end));
            }
        }
        throw new AccessException("Unsupported range access");
    }
    
    @Override
    public boolean canWrite(EvaluationContext context, Object target, Object index) 
        throws AccessException {
        return false; // Read-only for simplicity
    }
    
    @Override
    public void write(EvaluationContext context, Object target, Object index, Object newValue) 
        throws AccessException {
        throw new AccessException("Range write not supported");
    }
}

// Range class for index specification
public class Range {
    private final int start;
    private final int end;
    
    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
    
    public int getStart() { return start; }
    public int getEnd() { return end; }
}

// Usage would require custom expression parsing or function registration
StandardEvaluationContext context = new StandardEvaluationContext();
context.addIndexAccessor(new RangeIndexAccessor());

// This would require extending the parser or using functions
// Example conceptual usage: list[range(0, 2)] -> first 3 elements

{ .api }

Accessor Chain Management

Accessor Priority and Ordering

public class AccessorManager {
    
    public static StandardEvaluationContext createOptimizedContext() {
        StandardEvaluationContext context = new StandardEvaluationContext();
        
        // Add accessors in priority order (most specific first)
        context.addPropertyAccessor(new MapAccessor());               // Maps
        context.addPropertyAccessor(new DataBindingPropertyAccessor()); // Optimized
        context.addPropertyAccessor(new ReflectivePropertyAccessor());  // General
        
        context.addIndexAccessor(new ReflectiveIndexAccessor());        // General indexing
        
        return context;
    }
    
    public static void configureForSecurity(StandardEvaluationContext context) {
        // Clear default accessors and add only safe ones
        context.setPropertyAccessors(Arrays.asList(
            new SafePropertyAccessor(),
            new WhitelistPropertyAccessor()
        ));
        
        context.setIndexAccessors(Arrays.asList(
            new SafeIndexAccessor()
        ));
    }
}

public class SafePropertyAccessor implements PropertyAccessor {
    private final Set<Class<?>> allowedTypes = Set.of(
        String.class, Number.class, Boolean.class, Date.class
    );
    
    @Override
    public Class<?>[] getSpecificTargetClasses() {
        return allowedTypes.toArray(new Class<?>[0]);
    }
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, String name) {
        return allowedTypes.contains(target.getClass()) &&
               isAllowedProperty(name);
    }
    
    private boolean isAllowedProperty(String name) {
        // Whitelist approach
        return name.matches("^[a-zA-Z][a-zA-Z0-9]*$") && // Simple names only
               name.length() < 50;                        // Reasonable length
    }
    
    // ... implement other methods with safety checks
}

{ .api }

Performance Optimization

Cached Accessor Results

public class CachingPropertyAccessor implements PropertyAccessor {
    private final PropertyAccessor delegate;
    private final Map<String, TypedValue> readCache = new ConcurrentHashMap<>();
    private final long cacheTimeout;
    private final Map<String, Long> cacheTimestamps = new ConcurrentHashMap<>();
    
    public CachingPropertyAccessor(PropertyAccessor delegate, long cacheTimeoutMs) {
        this.delegate = delegate;
        this.cacheTimeout = cacheTimeoutMs;
    }
    
    @Override
    public Class<?>[] getSpecificTargetClasses() {
        return delegate.getSpecificTargetClasses();
    }
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, String name) {
        return delegate.canRead(context, target, name);
    }
    
    @Override
    public TypedValue read(EvaluationContext context, Object target, String name) 
        throws AccessException {
        String cacheKey = target.getClass().getName() + ":" + name;
        Long timestamp = cacheTimestamps.get(cacheKey);
        
        if (timestamp != null && 
            System.currentTimeMillis() - timestamp < cacheTimeout) {
            TypedValue cached = readCache.get(cacheKey);
            if (cached != null) {
                return cached;
            }
        }
        
        TypedValue result = delegate.read(context, target, name);
        readCache.put(cacheKey, result);
        cacheTimestamps.put(cacheKey, System.currentTimeMillis());
        
        return result;
    }
    
    @Override
    public boolean canWrite(EvaluationContext context, Object target, String name) {
        return delegate.canWrite(context, target, name);
    }
    
    @Override
    public void write(EvaluationContext context, Object target, String name, Object newValue) 
        throws AccessException {
        delegate.write(context, target, name, newValue);
        
        // Invalidate cache on write
        String cacheKey = target.getClass().getName() + ":" + name;
        readCache.remove(cacheKey);
        cacheTimestamps.remove(cacheKey);
    }
}

{ .api }

Specialized Accessors for Performance

// Highly optimized accessor for specific data structures
public class OptimizedDataObjectAccessor implements CompilablePropertyAccessor {
    
    @Override
    public Class<?>[] getSpecificTargetClasses() {
        return new Class<?>[] { OptimizedDataObject.class };
    }
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, String name) {
        return target instanceof OptimizedDataObject;
    }
    
    @Override
    public TypedValue read(EvaluationContext context, Object target, String name) 
        throws AccessException {
        OptimizedDataObject obj = (OptimizedDataObject) target;
        
        // Use optimized access methods
        return switch (name) {
            case "id" -> new TypedValue(obj.getId());
            case "name" -> new TypedValue(obj.getName());
            case "status" -> new TypedValue(obj.getStatus());
            default -> throw new AccessException("Unknown property: " + name);
        };
    }
    
    @Override
    public boolean canWrite(EvaluationContext context, Object target, String name) {
        return target instanceof OptimizedDataObject && 
               !"id".equals(name); // ID is read-only
    }
    
    @Override
    public void write(EvaluationContext context, Object target, String name, Object newValue) 
        throws AccessException {
        OptimizedDataObject obj = (OptimizedDataObject) target;
        
        switch (name) {
            case "name" -> obj.setName((String) newValue);
            case "status" -> obj.setStatus((String) newValue);
            default -> throw new AccessException("Cannot write property: " + name);
        }
    }
    
    // Compilation support for even better performance
    @Override
    public boolean isCompilable() {
        return true;
    }
    
    @Override
    public String generateCode(String propertyName, String target, CodeFlow cf) {
        // Generate bytecode for direct property access
        return switch (propertyName) {
            case "id" -> target + ".getId()";
            case "name" -> target + ".getName()";
            case "status" -> target + ".getStatus()";
            default -> null;
        };
    }
}

{ .api }

Best Practices

Security Considerations

// 1. Use whitelisting for property names
public class SecurePropertyAccessor implements PropertyAccessor {
    private final Set<String> allowedProperties;
    private final PropertyAccessor delegate;
    
    public SecurePropertyAccessor(PropertyAccessor delegate, String... allowedProperties) {
        this.delegate = delegate;
        this.allowedProperties = Set.of(allowedProperties);
    }
    
    @Override
    public boolean canRead(EvaluationContext context, Object target, String name) {
        return allowedProperties.contains(name) && 
               delegate.canRead(context, target, name);
    }
    
    // ... implement other methods with security checks
}

// 2. Validate input types and ranges
public class ValidatingIndexAccessor implements IndexAccessor {
    private final IndexAccessor delegate;
    
    @Override
    public TypedValue read(EvaluationContext context, Object target, Object index) 
        throws AccessException {
        validateIndex(index);
        return delegate.read(context, target, index);
    }
    
    private void validateIndex(Object index) throws AccessException {
        if (index instanceof Integer) {
            int idx = (Integer) index;
            if (idx < 0 || idx > 10000) { // Reasonable bounds
                throw new AccessException("Index out of safe range: " + idx);
            }
        }
    }
}

{ .api }

Performance Tips

  1. Order accessors by specificity: Place most specific accessors first in the chain
  2. Use targeted accessors: Implement getSpecificTargetClasses() to avoid unnecessary checks
  3. Cache expensive operations: Implement caching for costly property access
  4. Consider compilation: Implement CompilablePropertyAccessor for hot paths
  5. Minimize allocations: Reuse TypedValue instances when possible
  6. Profile accessor chains: Monitor which accessors are called most frequently

Error Handling

public class RobustPropertyAccessor implements PropertyAccessor {
    private final PropertyAccessor delegate;
    private final boolean failSilently;
    
    public RobustPropertyAccessor(PropertyAccessor delegate, boolean failSilently) {
        this.delegate = delegate;
        this.failSilently = failSilently;
    }
    
    @Override
    public TypedValue read(EvaluationContext context, Object target, String name) 
        throws AccessException {
        try {
            return delegate.read(context, target, name);
        } catch (Exception e) {
            if (failSilently) {
                return TypedValue.NULL; // Return null instead of throwing
            }
            throw new AccessException("Failed to read property '" + name + "'", e);
        }
    }
    
    // ... implement other methods with similar error handling
}

{ .api }

Install with Tessl CLI

npx tessl i tessl/maven-org-springframework--spring-expression

docs

advanced-features.md

error-handling.md

evaluation-contexts.md

expression-evaluation.md

index.md

method-constructor-resolution.md

property-index-access.md

spel-configuration.md

type-system-support.md

tile.json