Skip to content

Commit 0a1e8e5

Browse files
committed
Merge pull request #86 from scijava/tenacious-creation
Add a non-strict mode for context creation
2 parents 3ea252b + 47941df commit 0a1e8e5

File tree

3 files changed

+177
-24
lines changed

3 files changed

+177
-24
lines changed

src/main/java/org/scijava/Context.java

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@
5858
*/
5959
public class Context implements Disposable {
6060

61+
// -- Constants --
62+
63+
/**
64+
* System property indicating whether the context should fail fast when
65+
* is attempts to instantiate a required service which is invalid or missing.
66+
* If this property is set to "false" then the context creation will attempt
67+
* to continue even when a required service cannot be instantiated. Otherwise,
68+
* the constructor will throw an {@link IllegalArgumentException} in that situation.
69+
*/
70+
public static final String STRICT_PROPERTY = "scijava.context.strict";
71+
6172
// -- Fields --
6273

6374
/** Index of the application context's services. */
@@ -66,7 +77,11 @@ public class Context implements Disposable {
6677
/** Master index of all plugins known to the application context. */
6778
private final PluginIndex pluginIndex;
6879

69-
/** Creates a new SciJava application context with all available services. */
80+
/**
81+
* Creates a new SciJava application context with all available services.
82+
*
83+
* @see #Context(Collection, PluginIndex, boolean)
84+
*/
7085
public Context() {
7186
this(false);
7287
}
@@ -76,6 +91,7 @@ public Context() {
7691
*
7792
* @param empty If true, the context will be empty; otherwise, it will be
7893
* initialized with all available services.
94+
* @see #Context(Collection, PluginIndex, boolean)
7995
*/
8096
@SuppressWarnings("unchecked")
8197
public Context(final boolean empty) {
@@ -102,13 +118,12 @@ public Context(final boolean empty) {
102118
*
103119
* @param serviceClasses A list of types that implement the {@link Service}
104120
* interface (e.g., {@code DisplayService.class}).
121+
* @see #Context(Collection, PluginIndex, boolean)
105122
* @throws ClassCastException If any of the given arguments do not implement
106123
* the {@link Service} interface.
107124
*/
108-
@SuppressWarnings({ "rawtypes", "unchecked" })
109-
public Context(final Class... serviceClasses) {
110-
this(serviceClasses != null ? (Collection) Arrays.asList(serviceClasses)
111-
: Arrays.asList(Service.class));
125+
public Context(@SuppressWarnings("rawtypes") final Class... serviceClasses) {
126+
this(serviceClassList(serviceClasses));
112127
}
113128

114129
/**
@@ -117,34 +132,69 @@ public Context(final Class... serviceClasses) {
117132
*
118133
* @param serviceClasses A collection of types that implement the
119134
* {@link Service} interface (e.g., {@code DisplayService.class}).
135+
* @see #Context(Collection, PluginIndex, boolean)
120136
*/
121137
public Context(final Collection<Class<? extends Service>> serviceClasses) {
122138
this(serviceClasses, null);
123139
}
124140

141+
/**
142+
* Creates a new SciJava application context with the specified services (and
143+
* any required service dependencies).
144+
*
145+
* @param serviceClasses A collection of types that implement the
146+
* {@link Service} interface (e.g., {@code DisplayService.class}).
147+
* @param strict Whether context creation will fail fast when there is
148+
* is an error instantiating a required service.
149+
* @see #Context(Collection, PluginIndex, boolean)
150+
*/
151+
public Context(final Collection<Class<? extends Service>> serviceClasses,
152+
final boolean strict)
153+
{
154+
this(serviceClasses, null, strict);
155+
}
156+
125157
/**
126158
* Creates a new SciJava application with the specified PluginIndex. This
127159
* allows a base set of available plugins to be defined, and is useful when
128-
* plugins that would not be returned by the PluginIndex's PluginFinder are
129-
* desired.
130-
* <p>
131-
* NB: the {@link PluginIndex#discover()} method may still be called, adding
132-
* additional plugins to this index. The mechanism of discovery should be
133-
* configured exclusively through the attached
134-
* {@link org.scijava.plugin.PluginFinder}.
135-
* </p>
160+
* plugins that would not be returned by the {@link PluginIndex}'s
161+
* {@link org.scijava.plugin.PluginFinder} are desired.
136162
*
137163
* @param pluginIndex The plugin index to use when discovering and indexing
138164
* plugins. If you wish to completely control how services are
139165
* discovered (i.e., use your own
140166
* {@link org.scijava.plugin.PluginFinder} implementation), then you
141-
* can pass a custom {@link PluginIndex} here.
167+
* can pass a custom {@link PluginIndex} here. Passing null will
168+
* result in a default plugin index being constructed and used.
169+
* @see #Context(Collection, PluginIndex, boolean)
142170
*/
143171
@SuppressWarnings("unchecked")
144172
public Context(final PluginIndex pluginIndex) {
145173
this(Arrays.<Class<? extends Service>> asList(Service.class), pluginIndex);
146174
}
147175

176+
/**
177+
* Creates a new SciJava application context with the specified services (and
178+
* any required service dependencies). Service dependency candidates are
179+
* selected from those discovered by the given {@link PluginIndex}'s
180+
* associated {@link org.scijava.plugin.PluginFinder}.
181+
*
182+
* @param serviceClasses A collection of types that implement the
183+
* {@link Service} interface (e.g., {@code DisplayService.class}).
184+
* @param pluginIndex The plugin index to use when discovering and indexing
185+
* plugins. If you wish to completely control how services are
186+
* discovered (i.e., use your own
187+
* {@link org.scijava.plugin.PluginFinder} implementation), then you
188+
* can pass a custom {@link PluginIndex} here. Passing null will
189+
* result in a default plugin index being constructed and used.
190+
* @see #Context(Collection, PluginIndex, boolean)
191+
*/
192+
public Context(final Collection<Class<? extends Service>> serviceClasses,
193+
final PluginIndex pluginIndex)
194+
{
195+
this(serviceClasses, pluginIndex, strict());
196+
}
197+
148198
/**
149199
* Creates a new SciJava application context with the specified services (and
150200
* any required service dependencies). Service dependency candidates are
@@ -166,17 +216,21 @@ public Context(final PluginIndex pluginIndex) {
166216
* plugins. If you wish to completely control how services are
167217
* discovered (i.e., use your own
168218
* {@link org.scijava.plugin.PluginFinder} implementation), then you
169-
* can pass a custom {@link PluginIndex} here.
219+
* can pass a custom {@link PluginIndex} here. Passing null will
220+
* result in a default plugin index being constructed and used.
221+
* @param strict Whether context creation will fail fast when there is
222+
* is an error instantiating a required service.
170223
*/
171224
public Context(final Collection<Class<? extends Service>> serviceClasses,
172-
final PluginIndex pluginIndex)
225+
final PluginIndex pluginIndex, final boolean strict)
173226
{
174227
serviceIndex = new ServiceIndex();
175228

176229
this.pluginIndex = pluginIndex == null ? new PluginIndex() : pluginIndex;
177230
this.pluginIndex.discover();
178231

179-
final ServiceHelper serviceHelper = new ServiceHelper(this, serviceClasses);
232+
final ServiceHelper serviceHelper =
233+
new ServiceHelper(this, serviceClasses, strict);
180234
serviceHelper.loadServices();
181235
}
182236

@@ -323,6 +377,20 @@ public void dispose() {
323377
}
324378
}
325379

380+
// -- Utility methods --
381+
382+
/**
383+
* Utility method for converting a varargs list of service classes to a
384+
* {@link List} of those classes.
385+
*/
386+
@SuppressWarnings({ "rawtypes", "unchecked" })
387+
public static List<Class<? extends Service>> serviceClassList(
388+
final Class... serviceClasses)
389+
{
390+
return serviceClasses != null ? (List) Arrays.asList(serviceClasses)
391+
: Arrays.asList(Service.class);
392+
}
393+
326394
// -- Helper methods --
327395

328396
private String createMissingServiceMessage(
@@ -357,4 +425,8 @@ private String createMissingServiceMessage(
357425
return msg.toString();
358426
}
359427

428+
private static boolean strict() {
429+
return !"false".equals(System.getProperty(STRICT_PROPERTY));
430+
}
431+
360432
}

src/main/java/org/scijava/service/ServiceHelper.java

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ public class ServiceHelper extends AbstractContextual {
7171
/** Classes to instantiate as services. */
7272
private final List<Class<? extends Service>> serviceClasses;
7373

74+
/**
75+
* Whether service loading will fail fast when there is an error instantiating
76+
* a required service.
77+
*/
78+
private final boolean strict;
79+
7480
/**
7581
* Creates a new service helper for discovering and instantiating services.
7682
*
@@ -89,6 +95,21 @@ public ServiceHelper(final Context context) {
8995
*/
9096
public ServiceHelper(final Context context,
9197
final Collection<Class<? extends Service>> serviceClasses)
98+
{
99+
this(context, serviceClasses, true);
100+
}
101+
102+
/**
103+
* Creates a new service helper for discovering and instantiating services.
104+
*
105+
* @param context The application context to which services should be added.
106+
* @param serviceClasses The service classes to instantiate.
107+
* @param strict Whether service loading will fail fast when there is an error
108+
* instantiating a required service.
109+
*/
110+
public ServiceHelper(final Context context,
111+
final Collection<Class<? extends Service>> serviceClasses,
112+
final boolean strict)
92113
{
93114
setContext(context);
94115
log = context.getService(LogService.class);
@@ -108,6 +129,7 @@ public ServiceHelper(final Context context,
108129
// load only the services that were explicitly specified
109130
this.serviceClasses.addAll(serviceClasses);
110131
}
132+
this.strict = strict;
111133
}
112134

113135
// -- ServiceHelper methods --
@@ -207,15 +229,19 @@ private <S extends Service> S loadService(final Class<S> c,
207229
@SuppressWarnings("unchecked")
208230
final S result = (S) createExactService(serviceClass, required);
209231
if (required && result == null) {
210-
throw new IllegalArgumentException();
232+
final String error = "No match: " + serviceClass.getName();
233+
if (strict) throw new IllegalArgumentException(error);
234+
log.error(error);
211235
}
212236
return result;
213237
}
214238
}
215239

216240
if (required && c.isInterface()) {
217-
throw new IllegalArgumentException("No compatible service: " +
218-
c.getName());
241+
final String error = "No compatible service: " + c.getName();
242+
if (strict) throw new IllegalArgumentException(error);
243+
log.error(error);
244+
return null;
219245
}
220246

221247
return createExactService(c, required);
@@ -249,9 +275,11 @@ private <S extends Service> S createExactService(final Class<S> c,
249275
}
250276
catch (final Throwable t) {
251277
if (required) {
252-
throw new IllegalArgumentException("Invalid service: " + name, t);
278+
final String error = "Invalid service: " + name;
279+
if (strict) throw new IllegalArgumentException(error, t);
280+
log.error(error, t);
253281
}
254-
if (log.isDebug()) {
282+
else if (log.isDebug()) {
255283
// when in debug mode, give full stack trace of invalid services
256284
log.debug("Invalid service: " + name, t);
257285
}
@@ -290,8 +318,11 @@ private <S extends Service> S createServiceRecursively(final Class<S> c)
290318
continue;
291319
}
292320
if (!Service.class.isAssignableFrom(type)) {
293-
throw new IllegalArgumentException("Invalid parameter: " +
294-
f.getDeclaringClass().getName() + "#" + f.getName());
321+
final String error = "Invalid parameter: " +
322+
f.getDeclaringClass().getName() + "#" + f.getName();
323+
if (strict) throw new IllegalArgumentException(error);
324+
log.error(error);
325+
continue;
295326
}
296327
@SuppressWarnings("unchecked")
297328
final Class<? extends Service> serviceType =

src/test/java/org/scijava/ContextCreationTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import static org.junit.Assert.fail;
4040

4141
import java.util.Arrays;
42+
import java.util.List;
4243

4344
import org.junit.Test;
4445
import org.scijava.plugin.Parameter;
@@ -190,6 +191,55 @@ public void testOptionalMissingTransitive() {
190191
}
191192
}
192193

194+
/**
195+
* Tests that missing {@link Service}s are handled properly in non-strict
196+
* mode; specifically, that {@link IllegalArgumentException} is <em>not</em>
197+
* thrown when attempting to create a {@link Context} requiring one directly.
198+
*/
199+
@Test
200+
public void testNonStrictMissingDirect() {
201+
final List<Class<? extends Service>> serviceClasses =
202+
Context.serviceClassList(MissingService.class);
203+
final Context context = new Context(serviceClasses, false);
204+
assertEquals(0, context.getServiceIndex().size());
205+
}
206+
207+
/**
208+
* Tests that missing {@link Service}s are handled properly in non-strict
209+
* mode; specifically, that {@link IllegalArgumentException} is <em>not</em>
210+
* thrown when attempting to create a {@link Context} requiring one
211+
* transitively.
212+
*/
213+
@Test
214+
public void testNonStrictMissingTransitive() {
215+
final List<Class<? extends Service>> serviceClasses =
216+
Context.serviceClassList(MissingService.class);
217+
final Context context = new Context(serviceClasses, false);
218+
assertEquals(0, context.getServiceIndex().size());
219+
}
220+
221+
/**
222+
* Tests that missing-but-optional {@link Service}s are handled properly in
223+
* non-strict mode; specifically, that {@link IllegalArgumentException} is
224+
* <em>not</em> thrown when attempting to create a {@link Context} requiring
225+
* one transitively.
226+
* <p>
227+
* A service marked {@link Optional}, but annotated without
228+
* {@code required = false} from a dependent service, is assumed to be
229+
* required for that dependent service.
230+
* </p>
231+
*/
232+
@Test
233+
public void testNonStrictOptionalMissingTransitive() {
234+
final List<Class<? extends Service>> serviceClasses =
235+
Context.serviceClassList(ServiceRequiringOptionalMissingService.class);
236+
final Context context = new Context(serviceClasses, false);
237+
final List<Service> services = context.getServiceIndex().getAll();
238+
assertEquals(1, services.size());
239+
assertSame(ServiceRequiringOptionalMissingService.class, services.get(0)
240+
.getClass());
241+
}
242+
193243
/**
194244
* Verifies that the order plugins appear in the PluginIndex and Service list
195245
* does not affect which services are loaded.

0 commit comments

Comments
 (0)