Skip to content

Commit 67fad4b

Browse files
committed
Consolidate builder exceptions to BuildException
Pros: - Simpler API: Callers only need to handle one exception type. - Semantic clarity: "Building failed" vs. generic "I/O error" or "interrupted". - Flexibility: Wraps both IOException and InterruptedException with proper cause chain. - Industry standard: Gradle uses BuildException, Maven uses MojoExecutionException, etc. Cleaner than throws IOException, InterruptedException and follows the principle of "throw exceptions at the right level of abstraction."
1 parent 61f042e commit 67fad4b

20 files changed

+276
-163
lines changed

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
package org.apposed.appose;
3131

32+
import org.apposed.appose.builder.BuildException;
3233
import org.apposed.appose.builder.Builders;
3334
import org.apposed.appose.builder.DynamicBuilder;
3435
import org.apposed.appose.builder.MambaBuilder;
@@ -38,7 +39,6 @@
3839
import org.apposed.appose.util.Versions;
3940

4041
import java.io.File;
41-
import java.io.IOException;
4242

4343
/**
4444
* Appose is a library for interprocess cooperation with shared memory. The
@@ -267,7 +267,7 @@ public static PixiBuilder pixi() {
267267
* @param source Path to pixi.toml or environment.yml file.
268268
* @return A new PixiBuilder instance.
269269
*/
270-
public static PixiBuilder pixi(String source) throws IOException {
270+
public static PixiBuilder pixi(String source) throws BuildException {
271271
return new PixiBuilder(source);
272272
}
273273

@@ -286,8 +286,9 @@ public static MambaBuilder mamba() {
286286
*
287287
* @param source Path to environment.yml file.
288288
* @return A new MambaBuilder instance.
289+
* @throws BuildException If the source is incompatible with the mamba builder.
289290
*/
290-
public static MambaBuilder mamba(String source) throws IOException {
291+
public static MambaBuilder mamba(String source) throws BuildException {
291292
return new MambaBuilder(source);
292293
}
293294

@@ -306,8 +307,9 @@ public static UvBuilder uv() {
306307
*
307308
* @param source Path to requirements.txt file.
308309
* @return A new UvBuilder instance.
310+
* @throws BuildException If the source is incompatible with the uv builder.
309311
*/
310-
public static UvBuilder uv(String source) throws IOException {
312+
public static UvBuilder uv(String source) throws BuildException {
311313
return new UvBuilder(source);
312314
}
313315

@@ -344,11 +346,11 @@ public static DynamicBuilder content(String content) {
344346
*
345347
* @param envDir The directory containing the environment.
346348
* @return An Environment configured for the detected type.
347-
* @throws IOException If the directory doesn't exist or type cannot be determined.
349+
* @throws BuildException If the directory doesn't exist or type cannot be determined.
348350
*/
349-
public static Environment wrap(File envDir) throws IOException {
351+
public static Environment wrap(File envDir) throws BuildException {
350352
if (!envDir.exists()) {
351-
throw new IOException("Environment directory does not exist: " + envDir);
353+
throw new BuildException(null, "Environment directory does not exist: " + envDir);
352354
}
353355

354356
// Find a builder factory that can wrap this directory.
@@ -367,9 +369,9 @@ public static Environment wrap(File envDir) throws IOException {
367369
*
368370
* @param envDir The path to the directory containing the environment.
369371
* @return An Environment configured for the detected type.
370-
* @throws IOException If the directory doesn't exist or type cannot be determined.
372+
* @throws BuildException If the directory doesn't exist or type cannot be determined.
371373
*/
372-
public static Environment wrap(String envDir) throws IOException {
374+
public static Environment wrap(String envDir) throws BuildException {
373375
return wrap(new File(envDir));
374376
}
375377

@@ -388,9 +390,9 @@ public static Environment wrap(String envDir) throws IOException {
388390
* </ul>
389391
*
390392
* @return A system environment ready to use.
391-
* @throws IOException If the environment cannot be created.
393+
* @throws BuildException If the environment cannot be created.
392394
*/
393-
public static Environment system() throws IOException {
395+
public static Environment system() throws BuildException {
394396
return new SimpleBuilder()
395397
.inheritRunningJava()
396398
.appendSystemPath()

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

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929

3030
package org.apposed.appose;
3131

32+
import org.apposed.appose.builder.BuildException;
33+
3234
import java.io.File;
3335
import java.io.IOException;
3436
import java.net.URL;
@@ -67,9 +69,11 @@ public interface Builder<T extends Builder<T>> {
6769
* Builds the environment. This is the terminator method for any fluid building chain.
6870
*
6971
* @return The newly constructed Appose {@link Environment}.
70-
* @throws IOException If something goes wrong building the environment.
72+
* @throws BuildException if the build fails due to I/O errors,
73+
* interruption, or other build-related issues.
74+
* Check {@link BuildException#getCause()} for the underlying cause.
7175
*/
72-
Environment build() throws IOException;
76+
Environment build() throws BuildException;
7377

7478
/**
7579
* Rebuilds the environment from scratch.
@@ -83,10 +87,17 @@ public interface Builder<T extends Builder<T>> {
8387
* </p>
8488
*
8589
* @return The newly rebuilt {@link Environment}.
86-
* @throws IOException If something goes wrong during rebuild.
90+
* @throws BuildException if the build fails due to I/O errors,
91+
* interruption, or other build-related issues.
92+
* Check {@link BuildException#getCause()} for the underlying cause.
8793
*/
88-
default Environment rebuild() throws IOException {
89-
delete();
94+
default Environment rebuild() throws BuildException {
95+
try {
96+
delete();
97+
}
98+
catch (IOException e) {
99+
throw new BuildException(this, e);
100+
}
90101
return build();
91102
}
92103

@@ -107,11 +118,11 @@ default Environment rebuild() throws IOException {
107118
* found, it will be used when rebuild() is called later.
108119
* </p>
109120
*
110-
* @param envDir The existing environment directory to wrap
111-
* @return The wrapped {@link Environment}
112-
* @throws IOException If the directory doesn't exist or can't be wrapped
121+
* @param envDir The existing environment directory to wrap.
122+
* @return The wrapped {@link Environment}.
123+
* @throws BuildException If the directory doesn't exist or can't be wrapped.
113124
*/
114-
Environment wrap(File envDir) throws IOException;
125+
Environment wrap(File envDir) throws BuildException;
115126

116127
/**
117128
* Sets an environment variable to be passed to worker processes.
@@ -193,15 +204,20 @@ default T channels(String... channels) {
193204
*
194205
* @param path Path to configuration file (e.g., "pixi.toml", "environment.yml")
195206
* @return This builder instance, for fluent-style programming.
196-
* @throws IOException If the file cannot be read
207+
* @throws BuildException If the file cannot be read
197208
*/
198-
default T file(String path) throws IOException {
199-
java.nio.file.Path filePath = java.nio.file.Paths.get(path);
200-
String fileContent = new String(
201-
java.nio.file.Files.readAllBytes(filePath),
202-
java.nio.charset.StandardCharsets.UTF_8
203-
);
204-
return content(fileContent);
209+
default T file(String path) throws BuildException {
210+
try {
211+
java.nio.file.Path filePath = java.nio.file.Paths.get(path);
212+
String fileContent = new String(
213+
java.nio.file.Files.readAllBytes(filePath),
214+
java.nio.charset.StandardCharsets.UTF_8
215+
);
216+
return content(fileContent);
217+
}
218+
catch (IOException e) {
219+
throw new BuildException(this, e);
220+
}
205221
}
206222

207223
/**
@@ -219,9 +235,9 @@ default T file(String path) throws IOException {
219235
*
220236
* @param url URL to configuration file
221237
* @return This builder instance, for fluent-style programming.
222-
* @throws IOException If the URL cannot be read
238+
* @throws BuildException If the URL cannot be read
223239
*/
224-
default T url(URL url) throws IOException {
240+
default T url(URL url) throws BuildException {
225241
try (java.io.InputStream stream = url.openStream()) {
226242
java.io.ByteArrayOutputStream result = new java.io.ByteArrayOutputStream();
227243
byte[] buffer = new byte[8192];
@@ -232,6 +248,9 @@ default T url(URL url) throws IOException {
232248
String urlContent = result.toString(java.nio.charset.StandardCharsets.UTF_8.name());
233249
return content(urlContent);
234250
}
251+
catch (IOException e) {
252+
throw new BuildException(this, e);
253+
}
235254
}
236255

237256
/**

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929

3030
package org.apposed.appose;
3131

32+
import org.apposed.appose.builder.BuildException;
3233
import org.apposed.appose.builder.Builders;
3334

3435
import java.io.File;
35-
import java.io.IOException;
3636

3737
/**
3838
* Factory interface for creating builder instances.
@@ -57,7 +57,7 @@ public interface BuilderFactory {
5757
* @param source The source file path
5858
* @return A new configured builder instance
5959
*/
60-
Builder<?> createBuilder(String source) throws IOException;
60+
Builder<?> createBuilder(String source) throws BuildException;
6161

6262
/**
6363
* Creates a new builder instance configured with a source file and scheme.
@@ -66,7 +66,7 @@ public interface BuilderFactory {
6666
* @param scheme The scheme (e.g., "environment.yml", "pixi.toml")
6767
* @return A new configured builder instance
6868
*/
69-
Builder<?> createBuilder(String source, String scheme) throws IOException;
69+
Builder<?> createBuilder(String source, String scheme) throws BuildException;
7070

7171
/**
7272
* Returns the name of this builder (e.g., "pixi", "mamba", "system").

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
package org.apposed.appose;
3131

32+
import org.apposed.appose.builder.BuildException;
3233
import org.apposed.appose.util.FilePaths;
3334

3435
import java.io.File;
@@ -81,20 +82,25 @@ default String type() {
8182
* current builder configuration.
8283
*
8384
* @return The newly rebuilt environment.
84-
* @throws IOException If something goes wrong during rebuild.
85+
* @throws BuildException If something goes wrong during rebuild.
8586
*/
86-
default Environment rebuild() throws IOException {
87+
default Environment rebuild() throws BuildException {
8788
return builder().rebuild();
8889
}
8990

9091
/**
9192
* Deletes the existing environment directory, if any.
9293
*
9394
* @return This environment, for fluid chaining.
94-
* @throws IOException If something goes wrong during deletion.
95+
* @throws BuildException If something goes wrong during deletion.
9596
*/
96-
default Environment delete() throws IOException {
97-
builder().delete();
97+
default Environment delete() throws BuildException {
98+
try {
99+
builder().delete();
100+
}
101+
catch (IOException e) {
102+
throw new BuildException(builder(), e);
103+
}
98104
return this;
99105
}
100106

src/main/java/org/apposed/appose/builder/BaseBuilder.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,13 @@ public void delete() throws IOException {
7878
}
7979

8080
@Override
81-
public Environment wrap(File envDir) throws IOException {
82-
FilePaths.ensureDirectory(envDir);
81+
public Environment wrap(File envDir) throws BuildException {
82+
try {
83+
FilePaths.ensureDirectory(envDir);
84+
}
85+
catch (IOException e) {
86+
throw new BuildException(this, e);
87+
}
8388
// Set the base directory and build (which will detect existing env).
8489
base(envDir);
8590
return build();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*-
2+
* #%L
3+
* Appose: multi-language interprocess cooperation with shared memory.
4+
* %%
5+
* Copyright (C) 2023 - 2025 Appose developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.apposed.appose.builder;
31+
32+
import org.apposed.appose.Builder;
33+
import org.apposed.appose.Nullable;
34+
35+
/**
36+
* Exception thrown when a {@link Builder} fails to build an environment.
37+
*
38+
* @author Curtis Rueden
39+
*/
40+
public class BuildException extends Exception {
41+
42+
/** The builder associated with this exception. */
43+
@Nullable
44+
public final Builder<?> builder;
45+
46+
public BuildException(String message) {
47+
this(null, message);
48+
}
49+
50+
public BuildException(Throwable cause) {
51+
this(null, cause);
52+
}
53+
54+
public BuildException(@Nullable Builder<?> builder, String message) {
55+
super(message);
56+
this.builder = builder;
57+
}
58+
59+
public BuildException(@Nullable Builder<?> builder, Throwable cause) {
60+
this(builder, makeMessage(builder, cause), cause);
61+
}
62+
63+
public BuildException(@Nullable Builder<?> builder, String message, Throwable cause) {
64+
super(message, cause);
65+
this.builder = builder;
66+
}
67+
68+
private static String makeMessage(Builder<?> builder, Throwable cause) {
69+
String noun = builder == null ? "build" : builder.name() + " build";
70+
String verb = cause instanceof InterruptedException ? "interrupted" : "failed";
71+
return noun + " " + verb;
72+
}
73+
}

src/main/java/org/apposed/appose/builder/DynamicBuilder.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@
3333
import org.apposed.appose.BuilderFactory;
3434
import org.apposed.appose.Environment;
3535

36-
import java.io.IOException;
37-
3836
/**
3937
* Dynamic builder that auto-detects the appropriate specific builder
4038
* based on source file and scheme.
@@ -75,14 +73,14 @@ public String name() {
7573
}
7674

7775
@Override
78-
public Environment build() throws IOException {
76+
public Environment build() throws BuildException {
7977
Builder<?> delegate = createBuilder(builderName, source, scheme);
8078
copyConfigToDelegate(delegate);
8179
return delegate.build();
8280
}
8381

8482
@Override
85-
public Environment rebuild() throws IOException {
83+
public Environment rebuild() throws BuildException {
8684
Builder<?> delegate = createBuilder(builderName, source, scheme);
8785
copyConfigToDelegate(delegate);
8886
return delegate.rebuild();
@@ -103,7 +101,7 @@ private void copyConfigToDelegate(Builder<?> delegate) {
103101
errorSubscribers.forEach(delegate::subscribeError);
104102
}
105103

106-
private Builder<?> createBuilder(String name, String source, String scheme) throws IOException {
104+
private Builder<?> createBuilder(String name, String source, String scheme) throws BuildException {
107105
// Find the builder matching the specified name, if any.
108106
if (name != null) {
109107
BuilderFactory factory = Builders.findFactoryByName(name);

0 commit comments

Comments
 (0)