Skip to content

Commit 03c4037

Browse files
committed
better exceptions
1 parent 619d615 commit 03c4037

13 files changed

Lines changed: 178 additions & 41 deletions

src/main/java/dev/zarr/zarrjava/core/Array.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,14 @@ public void write(long[] offset, ucar.ma2.Array array, boolean parallel) {
122122
);
123123
}
124124
writeChunk(chunkCoords, chunkArray);
125-
} catch (ZarrException | InvalidRangeException e) {
126-
throw new RuntimeException(e);
125+
} catch (ZarrException e) {
126+
throw new RuntimeException(
127+
"Failed to write chunk at coordinates " + Arrays.toString(chunkCoords) +
128+
": " + e.getMessage(), e);
129+
} catch (InvalidRangeException e) {
130+
throw new RuntimeException(
131+
"Invalid array range when writing chunk at coordinates " + Arrays.toString(chunkCoords) +
132+
": " + e.getMessage(), e);
127133
}
128134
});
129135

src/main/java/dev/zarr/zarrjava/core/codec/CodecPipeline.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,9 @@ ArrayBytesCodec getArrayBytesCodec() {
6868
return (ArrayBytesCodec) codec;
6969
}
7070
}
71-
throw new RuntimeException(
72-
"Unreachable because the existence of exactly 1 ArrayBytes codec is asserted upon construction.");
71+
throw new IllegalStateException(
72+
"No ArrayBytesCodec found in codec pipeline. This should never happen as the existence " +
73+
"of exactly 1 ArrayBytesCodec is validated during construction.");
7374
}
7475

7576
BytesBytesCodec[] getBytesBytesCodecs() {

src/main/java/dev/zarr/zarrjava/core/codec/core/BytesCodec.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public ByteOrder getByteOrder() {
102102
case BIG:
103103
return ByteOrder.BIG_ENDIAN;
104104
default:
105-
throw new RuntimeException("Unreachable");
105+
throw new IllegalStateException("Unknown endian type: " + this);
106106
}
107107
}
108108
}

src/main/java/dev/zarr/zarrjava/store/BufferedZipStore.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,10 @@ public BufferedZipStore(@Nonnull StoreHandle underlyingStore, @Nonnull Store.Lis
5656
try {
5757
loadBuffer();
5858
} catch (IOException e) {
59-
throw new RuntimeException("Failed to load buffer from underlying store", e);
59+
throw StoreException.readFailed(
60+
underlyingStore.toString(),
61+
new String[]{},
62+
new IOException("Failed to load ZIP buffer from underlying store: " + underlyingStore, e));
6063
}
6164
}
6265

@@ -285,7 +288,10 @@ public void set(String[] keys, ByteBuffer bytes) {
285288
try {
286289
writeBuffer();
287290
} catch (IOException e) {
288-
throw new RuntimeException("Failed to flush buffer to underlying store after set operation", e);
291+
throw StoreException.writeFailed(
292+
underlyingStore.toString(),
293+
keys,
294+
new IOException("Failed to flush ZIP buffer to underlying store after set operation", e));
289295
}
290296
}
291297
}
@@ -297,7 +303,10 @@ public void delete(String[] keys) {
297303
try {
298304
writeBuffer();
299305
} catch (IOException e) {
300-
throw new RuntimeException("Failed to flush buffer to underlying store after delete operation", e);
306+
throw StoreException.deleteFailed(
307+
underlyingStore.toString(),
308+
keys,
309+
new IOException("Failed to flush ZIP buffer to underlying store after delete operation", e));
301310
}
302311
}
303312
}

src/main/java/dev/zarr/zarrjava/store/FilesystemStore.java

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,10 @@ public void set(String[] keys, ByteBuffer bytes) {
133133
try {
134134
Files.createDirectories(keyPath.getParent());
135135
} catch (IOException e) {
136-
throw new RuntimeException(e);
136+
throw StoreException.writeFailed(
137+
this.toString(),
138+
keys,
139+
new IOException("Failed to create parent directories for path: " + keyPath.getParent(), e));
137140
}
138141
try (SeekableByteChannel channel = Files.newByteChannel(keyPath.toAbsolutePath(),
139142
StandardOpenOption.CREATE,
@@ -142,18 +145,25 @@ public void set(String[] keys, ByteBuffer bytes) {
142145
)) {
143146
channel.write(bytes);
144147
} catch (IOException e) {
145-
throw new RuntimeException(e);
148+
throw StoreException.writeFailed(
149+
this.toString(),
150+
keys,
151+
new IOException("Failed to write " + bytes.remaining() + " bytes to file: " + keyPath, e));
146152
}
147153
}
148154

149155
@Override
150156
public void delete(String[] keys) {
157+
Path keyPath = resolveKeys(keys);
151158
try {
152-
Files.delete(resolveKeys(keys));
159+
Files.delete(keyPath);
153160
} catch (NoSuchFileException e) {
154-
// ignore
161+
// ignore - file doesn't exist, which is the desired outcome
155162
} catch (IOException e) {
156-
throw new RuntimeException(e);
163+
throw StoreException.deleteFailed(
164+
this.toString(),
165+
keys,
166+
new IOException("Failed to delete file: " + keyPath, e));
157167
}
158168
}
159169

@@ -180,7 +190,10 @@ public Stream<String[]> list(String[] prefix) {
180190
.filter(Files::isRegularFile)
181191
.map(path -> pathToKeyArray(rootPath, path, prefix));
182192
} catch (IOException e) {
183-
throw new RuntimeException("Failed to list store content", e);
193+
throw StoreException.listFailed(
194+
this.toString(),
195+
prefix,
196+
new IOException("Failed to walk directory tree at: " + rootPath, e));
184197
}
185198
}
186199

@@ -193,7 +206,10 @@ public Stream<String> listChildren(String[] prefix) {
193206
try {
194207
return Files.list(rootPath).map(path -> path.getFileName().toString());
195208
} catch (IOException e) {
196-
throw new RuntimeException("Failed to list store children", e);
209+
throw StoreException.listFailed(
210+
this.toString(),
211+
prefix,
212+
new IOException("Failed to list directory contents at: " + rootPath, e));
197213
}
198214
}
199215

@@ -219,7 +235,8 @@ public InputStream getInputStream(String[] keys, long start, long end) {
219235
if (start > 0) {
220236
long skipped = inputStream.skip(start);
221237
if (skipped < start) {
222-
throw new IOException("Unable to skip to the desired start position.");
238+
throw new IOException("Unable to skip to position " + start +
239+
", only skipped " + skipped + " bytes in file: " + keyPath);
223240
}
224241
}
225242
if (end != -1) {
@@ -229,17 +246,25 @@ public InputStream getInputStream(String[] keys, long start, long end) {
229246
return inputStream;
230247
}
231248
} catch (IOException e) {
232-
throw new RuntimeException(e);
249+
throw StoreException.readFailed(
250+
this.toString(),
251+
keys,
252+
new IOException("Failed to open input stream for file: " + keyPath +
253+
" (start: " + start + ", end: " + end + ")", e));
233254
}
234255
}
235256

236257
public long getSize(String[] keys) {
258+
Path keyPath = resolveKeys(keys);
237259
try {
238-
return Files.size(resolveKeys(keys));
260+
return Files.size(keyPath);
239261
} catch (NoSuchFileException e) {
240262
return -1;
241263
} catch (IOException e) {
242-
throw new RuntimeException(e);
264+
throw StoreException.readFailed(
265+
this.toString(),
266+
keys,
267+
new IOException("Failed to get file size for: " + keyPath, e));
243268
}
244269
}
245270
}

src/main/java/dev/zarr/zarrjava/store/HttpStore.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,12 @@ public void close() throws IOException {
148148

149149
@Override
150150
public long getSize(String[] keys) {
151+
String url = resolveKeys(keys);
151152
// Explicitly request "identity" encoding to prevent OkHttp from adding "gzip"
152153
// and subsequently stripping the Content-Length header.
153154
Request request = new Request.Builder()
154155
.head()
155-
.url(resolveKeys(keys))
156+
.url(url)
156157
.header("Accept-Encoding", "identity")
157158
.build();
158159

@@ -168,8 +169,16 @@ public long getSize(String[] keys) {
168169
return Long.parseLong(contentLength);
169170
}
170171
return -1;
172+
} catch (NumberFormatException e) {
173+
throw StoreException.readFailed(
174+
this.toString(),
175+
keys,
176+
new IOException("Invalid Content-Length header value from: " + url, e));
171177
} catch (IOException e) {
172-
throw new RuntimeException(e);
178+
throw StoreException.readFailed(
179+
this.toString(),
180+
keys,
181+
new IOException("Failed to get content length from HTTP HEAD request to: " + url, e));
173182
}
174183
}
175184
}

src/main/java/dev/zarr/zarrjava/store/ReadOnlyZipStore.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,10 @@ public long getSize(String[] keys) {
268268
// if size is not in header/cache, we fallback to reading
269269
InputStream inputStream = underlyingStore.getInputStream();
270270
if (inputStream == null) {
271-
throw new RuntimeException(new IOException("Underlying store input stream is null"));
271+
throw StoreException.readFailed(
272+
underlyingStore.toString(),
273+
keys,
274+
new IOException("Cannot get size - underlying store input stream is null"));
272275
}
273276
try (ZipArchiveInputStream zis = new ZipArchiveInputStream(inputStream)) {
274277
ZipArchiveEntry entry;
@@ -295,7 +298,10 @@ public long getSize(String[] keys) {
295298
}
296299
return -1; // file not found
297300
} catch (IOException e) {
298-
throw new RuntimeException(e);
301+
throw StoreException.readFailed(
302+
underlyingStore.toString(),
303+
keys,
304+
new IOException("Failed to read ZIP entry size for key: " + String.join("/", keys), e));
299305
}
300306
}
301307
}

src/main/java/dev/zarr/zarrjava/store/S3Store.java

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,20 @@ String resolveKeys(String[] keys) {
4242
ByteBuffer get(GetObjectRequest getObjectRequest) {
4343
try (ResponseInputStream<GetObjectResponse> inputStream = s3client.getObject(getObjectRequest)) {
4444
return Utils.asByteBuffer(inputStream);
45-
} catch (IOException e) {
45+
} catch (NoSuchKeyException e) {
46+
// Key doesn't exist, return null as per Store contract
4647
return null;
48+
} catch (S3Exception e) {
49+
// Include S3-specific error details
50+
throw StoreException.readFailed(
51+
this.toString(),
52+
new String[]{getObjectRequest.key()},
53+
new IOException("S3 error (code: " + e.statusCode() + "): " + e.awsErrorDetails().errorMessage(), e));
54+
} catch (IOException e) {
55+
throw StoreException.readFailed(
56+
this.toString(),
57+
new String[]{getObjectRequest.key()},
58+
new IOException("Failed to read S3 object content", e));
4759
}
4860
}
4961

@@ -91,19 +103,39 @@ public void set(String[] keys, ByteBuffer bytes) {
91103
// Convert ByteBuffer to byte array and use RequestBody.fromBytes()
92104
// This properly sets Content-Length and avoids buffering the entire stream in memory
93105
byte[] data = Utils.toArray(bytes);
94-
s3client.putObject(
95-
PutObjectRequest.builder()
96-
.bucket(bucketName)
97-
.key(resolveKeys(keys))
98-
.build(),
99-
RequestBody.fromBytes(data)
100-
);
106+
String key = resolveKeys(keys);
107+
try {
108+
s3client.putObject(
109+
PutObjectRequest.builder()
110+
.bucket(bucketName)
111+
.key(key)
112+
.build(),
113+
RequestBody.fromBytes(data)
114+
);
115+
} catch (S3Exception e) {
116+
throw StoreException.writeFailed(
117+
this.toString(),
118+
keys,
119+
new IOException("S3 putObject failed (code: " + e.statusCode() + ") for key '" + key +
120+
"', bucket '" + bucketName + "': " + e.awsErrorDetails().errorMessage(), e));
121+
}
101122
}
102123

103124
@Override
104125
public void delete(String[] keys) {
105-
s3client.deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(resolveKeys(keys))
106-
.build());
126+
String key = resolveKeys(keys);
127+
try {
128+
s3client.deleteObject(DeleteObjectRequest.builder()
129+
.bucket(bucketName)
130+
.key(key)
131+
.build());
132+
} catch (S3Exception e) {
133+
throw StoreException.deleteFailed(
134+
this.toString(),
135+
keys,
136+
new IOException("S3 deleteObject failed (code: " + e.statusCode() + ") for key '" + key +
137+
"', bucket '" + bucketName + "': " + e.awsErrorDetails().errorMessage(), e));
138+
}
107139
}
108140

109141
@Override
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package dev.zarr.zarrjava.store;
2+
3+
/**
4+
* Exception thrown when store operations fail.
5+
* Provides context about which store and operation failed.
6+
*/
7+
public class StoreException extends RuntimeException {
8+
9+
public StoreException(String message) {
10+
super(message);
11+
}
12+
13+
public StoreException(String message, Throwable cause) {
14+
super(message, cause);
15+
}
16+
17+
public static StoreException readFailed(String storePath, String[] keys, Throwable cause) {
18+
return new StoreException(
19+
String.format("Failed to read from store '%s' at key '%s': %s",
20+
storePath, String.join("/", keys), cause.getMessage()),
21+
cause);
22+
}
23+
24+
public static StoreException writeFailed(String storePath, String[] keys, Throwable cause) {
25+
return new StoreException(
26+
String.format("Failed to write to store '%s' at key '%s': %s",
27+
storePath, String.join("/", keys), cause.getMessage()),
28+
cause);
29+
}
30+
31+
public static StoreException deleteFailed(String storePath, String[] keys, Throwable cause) {
32+
return new StoreException(
33+
String.format("Failed to delete from store '%s' at key '%s': %s",
34+
storePath, String.join("/", keys), cause.getMessage()),
35+
cause);
36+
}
37+
38+
public static StoreException listFailed(String storePath, String[] keys, Throwable cause) {
39+
return new StoreException(
40+
String.format("Failed to list store contents at '%s' under key '%s': %s",
41+
storePath, String.join("/", keys), cause.getMessage()),
42+
cause);
43+
}
44+
}

src/main/java/dev/zarr/zarrjava/v3/Array.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ public static Array create(StoreHandle storeHandle, ArrayMetadata arrayMetadata,
143143
throws IOException, ZarrException {
144144
StoreHandle metadataHandle = storeHandle.resolve(ZARR_JSON);
145145
if (!existsOk && metadataHandle.exists()) {
146-
throw new RuntimeException(
147-
"Trying to create a new array in " + storeHandle + ". But " + metadataHandle
148-
+ " already exists.");
146+
throw new ZarrException(
147+
"Cannot create array at " + storeHandle + " - metadata file " + metadataHandle +
148+
" already exists. Use existsOk=true to overwrite.");
149149
}
150150
ObjectWriter objectWriter = makeObjectWriter();
151151
ByteBuffer metadataBytes = ByteBuffer.wrap(objectWriter.writeValueAsBytes(arrayMetadata));

0 commit comments

Comments
 (0)