Skip to content

Commit 719b91c

Browse files
committed
Improve input validation mechanism
1 parent d97d79c commit 719b91c

File tree

7 files changed

+102
-7
lines changed

7 files changed

+102
-7
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
</parent>
1111

1212
<artifactId>scijava-common</artifactId>
13-
<version>2.99.4-SNAPSHOT</version>
13+
<version>2.100.0-SNAPSHOT</version>
1414

1515
<name>SciJava Common</name>
1616
<description>SciJava Common is a shared library for SciJava software. It provides a plugin framework, with an extensible mechanism for service discovery, backed by its own annotation processor, so that plugins can be loaded dynamically. It is used by downstream projects in the SciJava ecosystem, such as ImageJ and SCIFIO.</description>

src/main/java/org/scijava/module/AbstractModule.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,13 @@ public void resolveInput(final String name) {
160160
item.validate(this);
161161
}
162162
catch (final MethodCallException exc) {
163-
// NB: Hacky, but avoids changing the API signature.
164-
throw new RuntimeException(exc);
163+
// NB: resolveInput cannot declare checked exceptions, so we wrap.
164+
// Prefer the cause's message (the user-facing validation error) when
165+
// available; otherwise fall back to the MethodCallException's message.
166+
final Throwable cause = exc.getCause();
167+
final String message = (cause != null && cause.getMessage() != null &&
168+
!cause.getMessage().isEmpty()) ? cause.getMessage() : exc.getMessage();
169+
throw new RuntimeException(message, exc);
165170
}
166171
}
167172
resolvedInputs.add(name);

src/main/java/org/scijava/module/AbstractModuleItem.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,12 @@ public void validate(final Module module) throws MethodCallException {
210210
if (validaterRef == null) {
211211
validaterRef = new MethodRef(delegateObject.getClass(), getValidater());
212212
}
213-
validaterRef.execute(module.getDelegateObject());
213+
final Object result = validaterRef.executeWithResult(module.getDelegateObject());
214+
// If the validater returns a non-empty String, treat it as an error message.
215+
if (result instanceof String) {
216+
final String message = (String) result;
217+
if (!message.isEmpty()) throw new MethodCallException(message);
218+
}
214219
}
215220

216221
@Override

src/main/java/org/scijava/module/MethodRef.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,15 @@ public MethodRef(final Class<?> clazz, final String methodName,
6262
public void execute(final Object obj, final Object... args)
6363
throws MethodCallException
6464
{
65-
if (method == null) return;
65+
executeWithResult(obj, args);
66+
}
67+
68+
public Object executeWithResult(final Object obj, final Object... args)
69+
throws MethodCallException
70+
{
71+
if (method == null) return null;
6672
try {
67-
method.invoke(obj, args);
73+
return method.invoke(obj, args);
6874
}
6975
catch (final Exception exc) {
7076
// NB: Several types of exceptions; simpler to handle them all the same.

src/main/java/org/scijava/module/ModuleItem.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,52 @@ public interface ModuleItem<T> extends BasicDetails {
126126

127127
/**
128128
* Invokes this item's validation function, if any, on the given module.
129-
*
129+
* <p>
130+
* The validation function may signal failure either by throwing an exception
131+
* or by returning a non-empty {@link String} error message.
132+
* </p>
133+
*
134+
* @throws MethodCallException if validation fails or the method cannot be
135+
* invoked. When the validater returns a non-empty String, a
136+
* {@link MethodCallException} is thrown with that string as its
137+
* message.
130138
* @see #getValidater()
139+
* @see #validateMessage(Module)
131140
*/
132141
void validate(Module module) throws MethodCallException;
133142

143+
/**
144+
* Validates this item's value in the given module, returning any error
145+
* message rather than throwing.
146+
* <p>
147+
* The validation function may signal failure either by throwing an exception
148+
* or by returning a non-empty {@link String} error message. This method
149+
* catches both cases and returns the error message as a string, or
150+
* {@code null} if the value is valid.
151+
* </p>
152+
*
153+
* @return an error message if the value is invalid, or {@code null} if valid.
154+
* @see #getValidater()
155+
* @see #validate(Module)
156+
*/
157+
default String validateMessage(final Module module) {
158+
try {
159+
validate(module);
160+
return null;
161+
}
162+
catch (final MethodCallException exc) {
163+
// Unwrap to find the most informative message.
164+
final Throwable cause = exc.getCause();
165+
if (cause != null && cause.getMessage() != null &&
166+
!cause.getMessage().isEmpty())
167+
{
168+
return cause.getMessage();
169+
}
170+
final String msg = exc.getMessage();
171+
return msg != null && !msg.isEmpty() ? msg : exc.toString();
172+
}
173+
}
174+
134175
/**
135176
* Gets the function that is called whenever this item changes.
136177
* <p>

src/main/java/org/scijava/widget/DefaultWidgetModel.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ public class DefaultWidgetModel extends AbstractContextual implements WidgetMode
7979

8080
private boolean initialized;
8181

82+
private String validationMessage;
83+
8284
public DefaultWidgetModel(final Context context, final InputPanel<?, ?> inputPanel,
8385
final Module module, final ModuleItem<?> item, final List<?> objectPool)
8486
{
@@ -167,6 +169,11 @@ public void setValue(final Object value) {
167169
if (initialized) {
168170
threadService.queue(() -> {
169171
callback();
172+
// Revalidate all inputs: changing one value may affect others' validity.
173+
for (final ModuleItem<?> anyItem : module.getInfo().inputs()) {
174+
final InputWidget<?, ?> w = inputPanel.getWidget(anyItem.getName());
175+
if (w != null) w.get().updateValidation();
176+
}
170177
inputPanel.refresh(); // must be on AWT thread?
171178
module.preview();
172179
});
@@ -284,6 +291,16 @@ public boolean isInitialized() {
284291
return initialized;
285292
}
286293

294+
@Override
295+
public String getValidationMessage() {
296+
return validationMessage;
297+
}
298+
299+
@Override
300+
public void updateValidation() {
301+
validationMessage = item.validateMessage(module);
302+
}
303+
287304
// -- Helper methods --
288305

289306
/**

src/main/java/org/scijava/widget/WidgetModel.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,27 @@ public interface WidgetModel extends Contextual {
187187
/** Gets whether the input is compatible with the given type. */
188188
boolean isType(Class<?> type);
189189

190+
/**
191+
* Gets the current validation error message for this widget.
192+
*
193+
* @return the error message from the most recent validation run, or
194+
* {@code null} if the last validation passed or no validation has
195+
* been run yet.
196+
* @see org.scijava.module.ModuleItem#validateMessage(org.scijava.module.Module)
197+
*/
198+
String getValidationMessage();
199+
200+
/**
201+
* Re-runs this item's validation and updates the stored validation message.
202+
* <p>
203+
* This should be called on all widgets whenever any parameter value changes,
204+
* since a change to one parameter may affect the validity of others.
205+
* </p>
206+
*
207+
* @see #getValidationMessage()
208+
*/
209+
void updateValidation();
210+
190211
/**
191212
* Toggles the widget's initialization state. An initialized widget can be
192213
* assumed to be an active part of a container {@link InputPanel}.

0 commit comments

Comments
 (0)