Skip to content

Commit 935e5b1

Browse files
ctruedenclaude
andcommitted
Add getAttribute for language-agnostic proxy attribute access
See also apposed/appose-python@93d1813. Unlike that commit, though, we do not get rid of ScriptSyntax's invokeMethod, because here in Java it is still important for method calls via type-safe proxy objects. Co-authored-by: Claude <[email protected]>
1 parent b61cdef commit 935e5b1

File tree

5 files changed

+192
-1
lines changed

5 files changed

+192
-1
lines changed

src/main/java/org/apposed/appose/ScriptSyntax.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,30 @@ public interface ScriptSyntax {
104104
* @see Service#proxy
105105
*/
106106
String invokeMethod(String objectVarName, String methodName, List<String> argVarNames);
107+
108+
/**
109+
* Generates a script expression to retrieve an attribute from an object.
110+
* <p>
111+
* The object must have been previously exported using {@code task.export()}.
112+
* This is used to access fields or obtain method references from remote objects.
113+
* </p>
114+
* <p>
115+
* The behavior depends on the language:
116+
* </p>
117+
* <ul>
118+
* <li><strong>Python:</strong> Returns the attribute value (field) or bound method object.</li>
119+
* <li><strong>Groovy:</strong> Tries field access first; if no such field exists,
120+
* returns a method reference using {@code .&} syntax.</li>
121+
* </ul>
122+
* <p>
123+
* If the result is a method reference, it will be auto-proxied as a {@link WorkerObject}
124+
* that can be called using {@link WorkerObject#call(Object...)}.
125+
* </p>
126+
*
127+
* @param objectVarName The name of the variable referencing the object.
128+
* @param attributeName The name of the attribute to retrieve.
129+
* @return A script expression that evaluates to the attribute value or method reference.
130+
* @see WorkerObject#getAttribute(String)
131+
*/
132+
String getAttribute(String objectVarName, String attributeName);
107133
}

src/main/java/org/apposed/appose/WorkerObject.java

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,11 @@ public String varName() {
112112
}
113113

114114
/**
115-
* Calls a method on the remote object.
115+
* Calls a method on the remote object by name.
116116
* <p>
117117
* This is a blocking operation that waits for the remote method to complete.
118+
* Uses {@link ScriptSyntax#invokeMethod(String, String, java.util.List)} to
119+
* generate the method invocation script.
118120
* </p>
119121
*
120122
* @param methodName The name of the method to invoke.
@@ -144,6 +146,106 @@ public Object call(String methodName, Object... args) throws InterruptedExceptio
144146
return task.result();
145147
}
146148

149+
/**
150+
* Invokes this remote object as a callable (function, method reference, or closure).
151+
* <p>
152+
* This is used when this WorkerObject represents a callable object, such as:
153+
* </p>
154+
* <ul>
155+
* <li>A method reference obtained via {@link #getAttribute(String)}</li>
156+
* <li>A function or closure</li>
157+
* <li>Any object with a {@code __call__} method (Python) or {@code call} method (Groovy)</li>
158+
* </ul>
159+
* <p>
160+
* This is a blocking operation that waits for the remote invocation to complete.
161+
* Uses {@link ScriptSyntax#call(String, java.util.List)} to generate the call script.
162+
* </p>
163+
* <p>
164+
* Example:
165+
* </p>
166+
* <pre>
167+
* WorkerObject obj = ...;
168+
* WorkerObject methodRef = (WorkerObject) obj.getAttribute("compute");
169+
* Object result = methodRef.invoke(arg1, arg2); // Invokes the method reference
170+
* </pre>
171+
*
172+
* @param args The arguments to pass to the callable.
173+
* @return The result of invoking the remote object.
174+
* @throws InterruptedException If the calling thread is interrupted while waiting.
175+
* @throws TaskException If the invocation fails in the worker process.
176+
*/
177+
public Object invoke(Object... args) throws InterruptedException, TaskException {
178+
// Build the call script using the service's syntax.
179+
java.util.Map<String, Object> inputs = new java.util.HashMap<>();
180+
java.util.List<String> argNames = new java.util.ArrayList<>();
181+
for (int i = 0; i < args.length; i++) {
182+
String argName = "arg" + i;
183+
inputs.put(argName, args[i]);
184+
argNames.add(argName);
185+
}
186+
187+
org.apposed.appose.syntax.Syntaxes.validate(service);
188+
String script = service.syntax().call(varName, argNames);
189+
190+
Service.Task task = service.task(script, inputs, queue);
191+
task.waitFor();
192+
if (task.status != Service.TaskStatus.COMPLETE) {
193+
throw new TaskException("Task failed: " + task.error, task);
194+
}
195+
return task.result();
196+
}
197+
198+
/**
199+
* Retrieves an attribute from the remote object.
200+
* <p>
201+
* The behavior depends on the worker language:
202+
* </p>
203+
* <ul>
204+
* <li><strong>Python:</strong> Returns the attribute value (field) or bound method object.</li>
205+
* <li><strong>Groovy:</strong> Tries field access first; if no such field exists,
206+
* returns a method reference (closure).</li>
207+
* </ul>
208+
* <p>
209+
* If the result is a method reference or other non-JSON-serializable object,
210+
* it will be auto-proxied as a {@code WorkerObject} that can be called using
211+
* {@link #call(Object...)}.
212+
* </p>
213+
* <p>
214+
* This is a blocking operation that waits for the remote operation to complete.
215+
* Uses {@link ScriptSyntax#getAttribute(String, String)} to generate the attribute
216+
* access script.
217+
* </p>
218+
* <p>
219+
* Example:
220+
* </p>
221+
* <pre>
222+
* WorkerObject obj = ...;
223+
*
224+
* // Field access - returns value directly
225+
* String label = (String) obj.getAttribute("label");
226+
*
227+
* // Method reference - returns WorkerObject that can be invoked
228+
* WorkerObject methodRef = (WorkerObject) obj.getAttribute("compute");
229+
* Object result = methodRef.invoke(arg1, arg2);
230+
* </pre>
231+
*
232+
* @param attributeName The name of the attribute to retrieve.
233+
* @return The attribute value (field) or a WorkerObject (method reference).
234+
* @throws InterruptedException If the calling thread is interrupted while waiting.
235+
* @throws TaskException If the attribute access fails in the worker process.
236+
*/
237+
public Object getAttribute(String attributeName) throws InterruptedException, TaskException {
238+
org.apposed.appose.syntax.Syntaxes.validate(service);
239+
String script = service.syntax().getAttribute(varName, attributeName);
240+
241+
Service.Task task = service.task(script, queue);
242+
task.waitFor();
243+
if (task.status != Service.TaskStatus.COMPLETE) {
244+
throw new TaskException("Task failed: " + task.error, task);
245+
}
246+
return task.result();
247+
}
248+
147249
/**
148250
* Creates a strongly-typed proxy for this remote object.
149251
* <p>

src/main/java/org/apposed/appose/syntax/GroovySyntax.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,15 @@ public String invokeMethod(String objectVarName, String methodName, List<String>
7474
// Groovy method invocation: object.method(arg0, arg1, ...)
7575
return objectVarName + "." + methodName + "(" + String.join(", ", argVarNames) + ")";
7676
}
77+
78+
@Override
79+
public String getAttribute(String objectVarName, String attributeName) {
80+
// Groovy attribute access: try field first, then method reference.
81+
// This handles the case where both field and method exist with same name:
82+
// field access takes precedence (Groovy semantics).
83+
// The .& syntax creates a method reference (closure) that can be called later.
84+
return "try { " + objectVarName + "." + attributeName + " } " +
85+
"catch (groovy.lang.MissingPropertyException e) { " +
86+
objectVarName + ".&" + attributeName + " }";
87+
}
7788
}

src/main/java/org/apposed/appose/syntax/PythonSyntax.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,11 @@ public String invokeMethod(String objectVarName, String methodName, List<String>
7373
// Python method invocation: object.method(arg0, arg1, ...)
7474
return objectVarName + "." + methodName + "(" + String.join(", ", argVarNames) + ")";
7575
}
76+
77+
@Override
78+
public String getAttribute(String objectVarName, String attributeName) {
79+
// Python attribute access: object.attribute
80+
// This returns either the field value or a bound method object.
81+
return objectVarName + "." + attributeName;
82+
}
7683
}

src/test/java/org/apposed/appose/SyntaxTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,51 @@ public void testProxy() throws Exception {
290290
}
291291
}
292292

293+
/** Tests getAttribute and invoke for field and method access. */
294+
@Test
295+
public void testGetAttributeGroovy() throws Exception {
296+
Environment env = Appose.system();
297+
try (Service service = env.groovy()) {
298+
maybeDebug(service);
299+
300+
// Create a class with both fields and methods
301+
Task setup = service.task(
302+
"class TestClass {\n" +
303+
" String fieldValue = 'field_data'\n" +
304+
" String methodValue() { return 'method_data' }\n" +
305+
" String compute(int x, int y) { return \"result: ${x + y}\" }\n" +
306+
"}\n" +
307+
"new TestClass()"
308+
).waitFor();
309+
assertComplete(setup);
310+
311+
// Get the WorkerObject
312+
WorkerObject testObj = (WorkerObject) setup.result();
313+
314+
// Test 1: Field access via getAttribute - should return value directly
315+
Object fieldResult = testObj.getAttribute("fieldValue");
316+
assertInstanceOf(String.class, fieldResult);
317+
assertEquals("field_data", fieldResult);
318+
319+
// Test 2: Method reference via getAttribute + invoke
320+
// methodValue has no field, so getAttribute returns method reference
321+
Object methodAttr = testObj.getAttribute("methodValue");
322+
assertInstanceOf(WorkerObject.class, methodAttr);
323+
Object methodResult = ((WorkerObject) methodAttr).invoke();
324+
assertEquals("method_data", methodResult);
325+
326+
// Test 3: Method reference with arguments
327+
Object computeAttr = testObj.getAttribute("compute");
328+
assertInstanceOf(WorkerObject.class, computeAttr);
329+
Object computeResult = ((WorkerObject) computeAttr).invoke(5, 7);
330+
assertEquals("result: 12", computeResult);
331+
332+
// Test 4: Compare with direct method invocation (existing functionality)
333+
Object directResult = testObj.call("compute", 3, 4);
334+
assertEquals("result: 7", directResult);
335+
}
336+
}
337+
293338
/** Tests automatic proxying of non-serializable task outputs. */
294339
@Test
295340
public void testAutoProxyGroovy() throws Exception {

0 commit comments

Comments
 (0)