Skip to content

Commit df765a2

Browse files
authored
Merge pull request #455 from InseeFr/fix/cast-exception
Fix cast exception (Fix #454)
2 parents 4a1bce2 + 9955ad7 commit df765a2

File tree

6 files changed

+248
-71
lines changed

6 files changed

+248
-71
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package fr.insee.vtl.engine.exceptions;
2+
3+
import fr.insee.vtl.model.Positioned;
4+
import fr.insee.vtl.model.exceptions.VtlScriptException;
5+
6+
/**
7+
* The <code>CastException</code> indicates that a cast operation failed during VTL expression
8+
* evaluation.
9+
*/
10+
public class CastException extends VtlScriptException {
11+
12+
/**
13+
* Constructor taking the parsing context and the message.
14+
*
15+
* @param message The exception message.
16+
* @param position The parsing context where the exception is thrown.
17+
*/
18+
public CastException(String message, Positioned position) {
19+
super(message, position);
20+
}
21+
22+
/**
23+
* Constructor taking the parsing context, the message and the mother exception.
24+
*
25+
* @param message The exception message.
26+
* @param cause The mother exception.
27+
* @param position The parsing context where the exception is thrown.
28+
*/
29+
public CastException(String message, Exception cause, Positioned position) {
30+
super(message + ": " + cause.getMessage(), position);
31+
initCause(cause);
32+
}
33+
}

vtl-engine/src/main/java/fr/insee/vtl/engine/expressions/CastExpression.java

Lines changed: 123 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package fr.insee.vtl.engine.expressions;
22

3+
import fr.insee.vtl.engine.exceptions.CastException;
34
import fr.insee.vtl.engine.exceptions.InvalidArgumentException;
5+
import fr.insee.vtl.engine.exceptions.VtlRuntimeException;
46
import fr.insee.vtl.model.Positioned;
57
import fr.insee.vtl.model.ResolvableExpression;
68
import fr.insee.vtl.model.exceptions.VtlScriptException;
@@ -76,7 +78,8 @@ public ResolvableExpression castBoolean(ResolvableExpression expr) {
7678
return exprValue ? 1D : 0D;
7779
});
7880
}
79-
throw new ClassCastException("Cast Boolean to " + outputClass + isNotSupported);
81+
throw new VtlRuntimeException(
82+
new CastException("Cast Boolean to " + outputClass + isNotSupported, this));
8083
}
8184

8285
private ResolvableExpression castDouble(ResolvableExpression expr) {
@@ -98,8 +101,9 @@ private ResolvableExpression castDouble(ResolvableExpression expr) {
98101
Double exprValue = (Double) expr.resolve(context);
99102
if (exprValue == null) return null;
100103
if (exprValue % 1 != 0)
101-
throw new UnsupportedOperationException(
102-
exprValue + " can not be casted into integer");
104+
throw new VtlRuntimeException(
105+
new CastException(
106+
exprValue + " can not be casted into integer", CastExpression.this));
103107
return exprValue.longValue();
104108
});
105109
if (outputClass.equals(Double.class))
@@ -115,7 +119,8 @@ private ResolvableExpression castDouble(ResolvableExpression expr) {
115119
if (exprValue == null) return null;
116120
return !exprValue.equals(0D);
117121
});
118-
throw new ClassCastException("Cast Double to " + outputClass + isNotSupported);
122+
throw new VtlRuntimeException(
123+
new CastException("Cast Double to " + outputClass + isNotSupported, this));
119124
}
120125

121126
private ResolvableExpression castInstant(ResolvableExpression expr, String mask) {
@@ -125,18 +130,24 @@ private ResolvableExpression castInstant(ResolvableExpression expr, String mask)
125130
.withPosition(expr)
126131
.using(
127132
context -> {
128-
var value = expr.resolve(context);
129-
Instant exprValue;
130-
if (value instanceof LocalDate date) {
131-
exprValue = date.atStartOfDay().toInstant(ZoneOffset.UTC);
132-
} else {
133-
exprValue = (Instant) value;
133+
try {
134+
var value = expr.resolve(context);
135+
Instant exprValue;
136+
if (value instanceof LocalDate date) {
137+
exprValue = date.atStartOfDay().toInstant(ZoneOffset.UTC);
138+
} else {
139+
exprValue = (Instant) value;
140+
}
141+
if (exprValue == null) return null;
142+
DateTimeFormatter maskFormatter = DateTimeFormatter.ofPattern(mask);
143+
return maskFormatter.format(exprValue.atOffset(ZoneOffset.UTC));
144+
} catch (Exception e) {
145+
throw new VtlRuntimeException(
146+
new CastException("Failed to cast date to string", e, CastExpression.this));
134147
}
135-
if (exprValue == null) return null;
136-
DateTimeFormatter maskFormatter = DateTimeFormatter.ofPattern(mask);
137-
return maskFormatter.format(exprValue.atOffset(ZoneOffset.UTC));
138148
});
139-
throw new ClassCastException("Cast Date to " + outputClass + isNotSupported);
149+
throw new VtlRuntimeException(
150+
new CastException("Cast Date to " + outputClass + isNotSupported, this));
140151
}
141152

142153
private ResolvableExpression castLong(ResolvableExpression expr) {
@@ -168,7 +179,8 @@ private ResolvableExpression castLong(ResolvableExpression expr) {
168179
if (exprValue == null) return null;
169180
return !exprValue.equals(0L);
170181
});
171-
throw new ClassCastException("Cast Long to " + outputClass + isNotSupported);
182+
throw new VtlRuntimeException(
183+
new CastException("Cast Long to " + outputClass + isNotSupported, this));
172184
}
173185

174186
private ResolvableExpression castString(ResolvableExpression expr, String mask) {
@@ -178,18 +190,54 @@ private ResolvableExpression castString(ResolvableExpression expr, String mask)
178190
.withPosition(expr)
179191
.using(
180192
context -> {
181-
String exprValue = (String) expr.resolve(context);
182-
if (exprValue == null) return null;
183-
return Long.valueOf(exprValue);
193+
String exprValue = null;
194+
try {
195+
exprValue = (String) expr.resolve(context);
196+
if (exprValue == null) return null;
197+
if (exprValue.isEmpty()) {
198+
throw new VtlRuntimeException(
199+
new CastException(
200+
"Cannot cast empty string \"\" to integer", CastExpression.this));
201+
}
202+
return Long.valueOf(exprValue);
203+
} catch (VtlRuntimeException e) {
204+
// Re-throw VtlRuntimeException as-is
205+
throw e;
206+
} catch (NumberFormatException e) {
207+
String value = exprValue != null ? exprValue : "null";
208+
throw new VtlRuntimeException(
209+
new CastException(
210+
"Failed to cast string \"" + value + "\" to integer",
211+
e,
212+
CastExpression.this));
213+
}
184214
});
185215
} else if (outputClass.equals(Double.class)) {
186216
return ResolvableExpression.withType(Double.class)
187217
.withPosition(expr)
188218
.using(
189219
context -> {
190-
String exprValue = (String) expr.resolve(context);
191-
if (exprValue == null) return null;
192-
return Double.valueOf(exprValue);
220+
String exprValue = null;
221+
try {
222+
exprValue = (String) expr.resolve(context);
223+
if (exprValue == null) return null;
224+
if (exprValue.isEmpty()) {
225+
throw new VtlRuntimeException(
226+
new CastException(
227+
"Cannot cast empty string \"\" to number", CastExpression.this));
228+
}
229+
return Double.valueOf(exprValue);
230+
} catch (VtlRuntimeException e) {
231+
// Re-throw VtlRuntimeException as-is
232+
throw e;
233+
} catch (NumberFormatException e) {
234+
String value = exprValue != null ? exprValue : "null";
235+
throw new VtlRuntimeException(
236+
new CastException(
237+
"Failed to cast string \"" + value + "\" to number",
238+
e,
239+
CastExpression.this));
240+
}
193241
});
194242
} else if (outputClass.equals(Boolean.class)) {
195243
return ResolvableExpression.withType(Boolean.class)
@@ -205,42 +253,76 @@ private ResolvableExpression castString(ResolvableExpression expr, String mask)
205253
.withPosition(expr)
206254
.using(
207255
context -> {
208-
if (mask == null) return null;
209-
String exprValue = (String) expr.resolve(context);
210-
if (exprValue == null) return null;
211-
// The spec is pretty vague about date and time. Apparently, date is a point in time
212-
// so a good java
213-
// representation is Instant. But date can be created using only year/month and date
214-
// mask, leaving
215-
// any time information.
216-
DateTimeFormatter maskFormatter =
217-
DateTimeFormatter.ofPattern(mask).withZone(ZoneOffset.UTC);
256+
String exprValue = null;
218257
try {
219-
return LocalDateTime.parse(exprValue, maskFormatter).toInstant(ZoneOffset.UTC);
220-
} catch (DateTimeParseException dtp) {
221-
return LocalDate.parse(exprValue, maskFormatter)
222-
.atStartOfDay()
223-
.toInstant(ZoneOffset.UTC);
258+
if (mask == null) return null;
259+
exprValue = (String) expr.resolve(context);
260+
if (exprValue == null) return null;
261+
if (exprValue.isEmpty()) {
262+
throw new VtlRuntimeException(
263+
new CastException(
264+
"Cannot cast empty string \"\" to date (mask: \"" + mask + "\")",
265+
CastExpression.this));
266+
}
267+
// The spec is pretty vague about date and time. Apparently, date is a point
268+
// in time so a good Java representation is Instant. But date can be created
269+
// using only year/month and date mask, leaving any time information.
270+
DateTimeFormatter maskFormatter =
271+
DateTimeFormatter.ofPattern(mask).withZone(ZoneOffset.UTC);
272+
try {
273+
return LocalDateTime.parse(exprValue, maskFormatter).toInstant(ZoneOffset.UTC);
274+
} catch (DateTimeParseException dtp) {
275+
return LocalDate.parse(exprValue, maskFormatter)
276+
.atStartOfDay()
277+
.toInstant(ZoneOffset.UTC);
278+
}
279+
} catch (VtlRuntimeException e) {
280+
// Re-throw VtlRuntimeException as-is
281+
throw e;
282+
} catch (Exception e) {
283+
String value = exprValue != null ? exprValue : "null";
284+
throw new VtlRuntimeException(
285+
new CastException(
286+
"Failed to cast string \""
287+
+ value
288+
+ "\" to date with mask \""
289+
+ mask
290+
+ "\"",
291+
e,
292+
CastExpression.this));
224293
}
225294
});
226295
} else if (outputClass.equals(PeriodDuration.class)) {
227296
return ResolvableExpression.withType(PeriodDuration.class)
228297
.withPosition(expr)
229298
.using(
230299
context -> {
231-
String value = (String) expr.tryCast(String.class).resolve(context);
232-
return PeriodDuration.parse(value).normalizedYears().normalizedStandardDays();
300+
try {
301+
String value = (String) expr.tryCast(String.class).resolve(context);
302+
return PeriodDuration.parse(value).normalizedYears().normalizedStandardDays();
303+
} catch (Exception e) {
304+
throw new VtlRuntimeException(
305+
new CastException(
306+
"Failed to cast string to duration", e, CastExpression.this));
307+
}
233308
});
234309
} else if (outputClass.equals(Interval.class)) {
235310
return ResolvableExpression.withType(Interval.class)
236311
.withPosition(expr)
237312
.using(
238313
context -> {
239-
String value = (String) expr.tryCast(String.class).resolve(context);
240-
return Interval.parse(value);
314+
try {
315+
String value = (String) expr.tryCast(String.class).resolve(context);
316+
return Interval.parse(value);
317+
} catch (Exception e) {
318+
throw new VtlRuntimeException(
319+
new CastException(
320+
"Failed to cast string to interval", e, CastExpression.this));
321+
}
241322
});
242323
} else {
243-
throw new ClassCastException("Cast String to " + outputClass + isNotSupported);
324+
throw new VtlRuntimeException(
325+
new CastException("Cast String to " + outputClass + isNotSupported, this));
244326
}
245327
}
246328

vtl-engine/src/test/java/fr/insee/vtl/engine/visitors/expression/functions/GenericFunctionsTest.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55

6+
import fr.insee.vtl.engine.exceptions.CastException;
67
import fr.insee.vtl.engine.exceptions.InvalidArgumentException;
78
import java.time.Instant;
89
import java.time.Period;
@@ -126,14 +127,14 @@ public void testCastExprDataset() throws ScriptException {
126127
() -> {
127128
engine.eval("g := cast(\"\", integer);");
128129
})
129-
.isInstanceOf(NumberFormatException.class)
130-
.hasMessage("For input string: \"\"");
130+
.isInstanceOf(CastException.class)
131+
.hasMessage("Cannot cast empty string \"\" to integer");
131132
assertThatThrownBy(
132133
() -> {
133134
engine.eval("h := cast(\"\", number);");
134135
})
135-
.isInstanceOf(NumberFormatException.class)
136-
.hasMessage("empty String");
136+
.isInstanceOf(CastException.class)
137+
.hasMessage("Cannot cast empty string \"\" to number");
137138

138139
// Cast Boolean to...
139140
engine.eval("i := cast(true, integer);");
@@ -170,7 +171,7 @@ public void testCastExprDataset() throws ScriptException {
170171
() -> {
171172
engine.eval("v := cast(1.1, integer);");
172173
})
173-
.isInstanceOf(UnsupportedOperationException.class)
174+
.isInstanceOf(CastException.class)
174175
.hasMessage("1.1 can not be casted into integer");
175176
engine.eval("w := cast(1.1, number);");
176177
assertThat(context.getAttribute("w")).isEqualTo(1.1D);

vtl-spark/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
<groupId>fr.insee.trevas</groupId>
3333
<artifactId>vtl-engine</artifactId>
3434
<version>2.1.0-SNAPSHOT</version>
35-
<scope>test</scope>
35+
<scope>compile</scope>
3636
</dependency>
3737
<dependency>
3838
<groupId>org.apache.spark</groupId>

0 commit comments

Comments
 (0)