2626
2727import org .citrusframework .context .TestContext ;
2828import org .citrusframework .exceptions .CitrusRuntimeException ;
29+ import org .citrusframework .exceptions .SegmentEvaluationException ;
2930import org .citrusframework .spi .ResourcePathTypeResolver ;
3031import org .citrusframework .spi .TypeResolver ;
3132import org .citrusframework .util .ReflectionHelper ;
@@ -53,7 +54,6 @@ public class SegmentVariableExtractorRegistry {
5354 * Resolves extractor from resource path lookup with given extractor resource name. Scans classpath for extractor meta information
5455 * with given name and returns instance of extractor. Returns optional instead of throwing exception when no extractor
5556 * could be found.
56- * @return
5757 */
5858 static Collection <SegmentVariableExtractor > lookup () {
5959 try {
@@ -69,16 +69,16 @@ static Collection<SegmentVariableExtractor> lookup() {
6969 /**
7070 * SegmentVariableExtractors to extract values from value representations of individual segments.
7171 */
72- private final List <SegmentVariableExtractor > segmentValueExtractors = new ArrayList <>(List .of (MapVariableExtractor .INSTANCE , ObjectFieldValueExtractor .INSTANCE ));
72+ private final List <SegmentVariableExtractor > segmentValueExtractors = new ArrayList <>(List .of (
73+ MapVariableExtractor .INSTANCE , ObjectFieldValueExtractor .INSTANCE ));
7374
7475 public SegmentVariableExtractorRegistry () {
7576 segmentValueExtractors .addAll (lookup ());
7677 }
7778
7879 /**
7980 * Obtain the segment variable extractors managed by the registry
80- *
81- * @return
81+
8282 */
8383 public List <SegmentVariableExtractor > getSegmentValueExtractors () {
8484 return segmentValueExtractors ;
@@ -87,140 +87,203 @@ public List<SegmentVariableExtractor> getSegmentValueExtractors() {
8787 /**
8888 * Base class for segment variable extractors that ensures that an exception is thrown upon no match.
8989 */
90- public static abstract class AbstractSegmentVariableExtractor implements SegmentVariableExtractor {
90+ public abstract static class AbstractSegmentVariableExtractor implements SegmentVariableExtractor {
9191
9292 @ Override
9393 public final Object extractValue (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher ) {
94- Object matchedValue = doExtractValue (testContext , object , matcher );
9594
96- if (matchedValue == null ) {
97- handleMatchFailure (matcher );
95+ try {
96+ return doExtractValue (testContext , object , matcher );
97+ } catch (SegmentEvaluationException e ) {
98+ throw createMatchFailureException (matcher , object , e );
9899 }
99-
100- return matchedValue ;
101100 }
102101
103102 /**
104- * Handles a match failure by throwing a CitrusException with an appropriate message
105- * @param matcher
103+ * Builds a {@link CitrusRuntimeException} describing why a variable/segment could not be resolved.
106104 */
107- private void handleMatchFailure (VariableExpressionSegmentMatcher matcher ) {
108- String exceptionMessage ;
109- if (matcher .getTotalSegmentCount () == 1 ) {
110- exceptionMessage = String . format ( "Unknown variable '%s'" ,
111- matcher .getVariableExpression ());
112- } else {
113- if ( matcher . getSegmentIndex () == 1 ) {
114- exceptionMessage = String . format ( "Unknown variable for first segment '%s' " +
115- "of variable expression '%s'" ,
116- matcher . getSegmentExpression (), matcher . getVariableExpression ());
117- } else {
118- exceptionMessage = String . format ( "Unknown segment-value for segment '%s' " +
119- "of variable expression '%s'" ,
120- matcher . getSegmentExpression (), matcher . getVariableExpression ());
121- }
105+ private static CitrusRuntimeException createMatchFailureException (VariableExpressionSegmentMatcher matcher , Object object , SegmentEvaluationException cause ) {
106+
107+ String expr = nullSafe (matcher .getVariableExpression ());
108+ String segment = nullSafe ( matcher . getSegmentExpression ());
109+ int idx = safeIndex ( matcher .getSegmentIndex ());
110+ int total = safeIndex ( matcher . getTotalSegmentCount ());
111+
112+ String objectType = ( object == null ) ? "null" : object . getClass (). getName ();
113+
114+ StringBuilder sb = new StringBuilder ( 256 )
115+ . append ( "Unable to extract value using expression '" ). append ( expr ). append ( "'" );
116+
117+ if ( total > 1 && idx >= 1 && idx <= total ) {
118+ sb . append ( " — failed at segment '" ). append ( segment )
119+ . append ( "' (" ). append ( idx ). append ( '/' ). append ( total ). append ( ')' );
122120 }
123- throw new CitrusRuntimeException (exceptionMessage );
121+
122+ if (cause != null ) {
123+ sb .append (String .format ("%nReason: %s" ,
124+ cause .getMessage () == null ? "" : cause .getMessage ()
125+ ));
126+ }
127+
128+ sb .append (String .format ("%nFrom object (%s):%n%s" , objectType , cause != null ? cause .getRenderedObject () : "" ));
129+
130+ return new CitrusRuntimeException (sb .toString ());
124131 }
125132
126- protected abstract Object doExtractValue (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher );
133+ protected abstract Object doExtractValue (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher ) throws SegmentEvaluationException ;
134+
135+ private static String nullSafe (String s ) { return s == null ? "<null>" : s ; }
136+
137+ private static int safeIndex (int i ) { return Math .max (0 , i ); }
138+ }
139+
140+
141+
142+ /** Minimal, safe rendering used as fallback (truncate huge payloads). */
143+ protected static String renderObjectMinimal (Object object ) {
144+ if (object == null ) return "null" ;
145+ return String .valueOf (object );
127146 }
128147
129148 /**
130- * Base class for extractors that can operate on indexed values .
149+ * Base class for extractors that support an optional [index] on the segment .
131150 */
132- public static abstract class IndexedSegmentVariableExtractor extends AbstractSegmentVariableExtractor {
151+ public abstract static class IndexedSegmentVariableExtractor extends AbstractSegmentVariableExtractor {
133152
134- public final Object doExtractValue (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher ) {
153+ @ Override
154+ public final Object doExtractValue (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher )
155+ throws SegmentEvaluationException {
135156
136157 Object extractedValue = doExtractIndexedValue (testContext , object , matcher );
137158
138159 if (matcher .getSegmentIndex () != -1 ) {
139- extractedValue = getIndexedElement (matcher , extractedValue );
160+ extractedValue = getIndexedElement (object , matcher , extractedValue );
140161 }
141162 return extractedValue ;
142163 }
143164
144165 /**
145- * Get the index element from an indexed value.
146- *
147- * @param matcher
148- * @param indexedValue
149- * @return
166+ * Return the element at the given index from arrays or lists. Throw SegmentEvaluationException for errors.
150167 */
151- private Object getIndexedElement (VariableExpressionSegmentMatcher matcher , Object indexedValue ) {
168+ private Object getIndexedElement (Object root , VariableExpressionSegmentMatcher matcher , Object indexedValue )
169+ throws SegmentEvaluationException {
170+
171+ int idx = matcher .getSegmentIndex ();
172+
173+ if (indexedValue == null ) {
174+ throw new SegmentEvaluationException (
175+ String .format ("Cannot index into null for segment '%s' (index %d)" ,
176+ matcher .getSegmentExpression (), idx ),
177+ renderObjectMinimal (root ));
178+ }
179+
180+ // Java array
152181 if (indexedValue .getClass ().isArray ()) {
153- return Array .get (indexedValue , matcher .getSegmentIndex ());
154- } else {
155- throw new CitrusRuntimeException (
156- String .format ("Expected an instance of Array type. Cannot retrieve indexed property %s from %s " ,
157- matcher .getSegmentExpression (), indexedValue .getClass ().getName ()));
182+ int length = Array .getLength (indexedValue );
183+ if (idx < 0 || idx >= length ) {
184+ throw new SegmentEvaluationException (
185+ String .format ("Index %d out of bounds (array length %d) for segment '%s'" ,
186+ idx , length , matcher .getSegmentExpression ()),
187+ renderObjectMinimal (root ));
188+ }
189+ return Array .get (indexedValue , idx );
190+ }
191+
192+ // java.util.List
193+ if (indexedValue instanceof List <?> list ) {
194+ int length = list .size ();
195+ if (idx < 0 || idx >= length ) {
196+ throw new SegmentEvaluationException (
197+ String .format ("Index %d out of bounds (list size %d) for segment '%s'" ,
198+ idx , length , matcher .getSegmentExpression ()),
199+ renderObjectMinimal (root ));
200+ }
201+ return list .get (idx );
158202 }
203+
204+ // Unsupported type
205+ throw new SegmentEvaluationException (
206+ String .format ("Expected array or List for indexed access, but was %s (segment '%s')" ,
207+ indexedValue .getClass ().getName (), matcher .getSegmentExpression ()),
208+ renderObjectMinimal (root ));
159209 }
160210
161- /**
162- * Extract the indexed value from the object
163- *
164- * @param object
165- * @param matcher
166- * @return
167- */
168- protected abstract Object doExtractIndexedValue (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher );
211+ /** Implement in subclasses: extract the (possibly indexed) container value to index into. */
212+ protected abstract Object doExtractIndexedValue (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher )
213+ throws SegmentEvaluationException ;
169214 }
170215
171216 /**
172- * SegmentVariableExtractor that accesses the segment value by a {@link Field} of the parentObject
217+ * Extracts a segment via a declared field on the parent object.
173218 */
174219 public static class ObjectFieldValueExtractor extends IndexedSegmentVariableExtractor {
175220
176- public static ObjectFieldValueExtractor INSTANCE = new ObjectFieldValueExtractor ();
177-
178- private ObjectFieldValueExtractor () {
179- // singleton
180- }
221+ public static final ObjectFieldValueExtractor INSTANCE = new ObjectFieldValueExtractor ();
222+ private ObjectFieldValueExtractor () {}
181223
182224 @ Override
183- protected Object doExtractIndexedValue (TestContext testContext , Object parentObject , VariableExpressionSegmentMatcher matcher ) {
184- Field field = ReflectionHelper .findField (parentObject .getClass (), matcher .getSegmentExpression ());
185- if (field == null ) {
186- throw new CitrusRuntimeException (String .format ("Failed to get variable - unknown field '%s' on type %s" ,
187- matcher .getSegmentExpression (), parentObject .getClass ().getName ()));
225+ protected Object doExtractIndexedValue (TestContext testContext , Object parentObject , VariableExpressionSegmentMatcher matcher )
226+ throws SegmentEvaluationException {
227+ try {
228+ Field field = ReflectionHelper .findField (parentObject .getClass (), matcher .getSegmentExpression ());
229+ if (field == null ) {
230+ throw new SegmentEvaluationException (
231+ String .format ("Unknown field '%s' on type %s" ,
232+ matcher .getSegmentExpression (), parentObject .getClass ().getName ()),
233+ renderObjectMinimal (parentObject ));
234+ }
235+ return ReflectionHelper .getField (field , parentObject );
236+ } catch (SegmentEvaluationException see ) {
237+ throw see ; // rethrow as-is
238+ } catch (Exception ex ) {
239+ throw new SegmentEvaluationException (
240+ String .format ("Failed to access field '%s' on type %s: %s" ,
241+ matcher .getSegmentExpression (), parentObject .getClass ().getName (), ex .getMessage ()),
242+ renderObjectMinimal (parentObject ));
188243 }
189-
190- return ReflectionHelper .getField (field , parentObject );
191244 }
192245
193246 @ Override
194247 public boolean canExtract (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher ) {
248+ // Objects except Strings (JSON/XML strings handled by dedicated extractors)
195249 return object != null && !(object instanceof String );
196250 }
197251 }
198252
199253 /**
200- * SegmentVariableExtractor that accesses the segment value from a {@link Map}. The extractor uses the segment expression
201- * as key into the map.
254+ * Extracts a segment via Map lookup using the segment expression as key.
202255 */
203256 public static class MapVariableExtractor extends IndexedSegmentVariableExtractor {
204257
205- public static MapVariableExtractor INSTANCE = new MapVariableExtractor ();
206-
207- private MapVariableExtractor () {
208- // singleton
209- }
258+ public static final MapVariableExtractor INSTANCE = new MapVariableExtractor ();
259+ private MapVariableExtractor () {}
210260
211261 @ Override
212- protected Object doExtractIndexedValue (TestContext testContext , Object parentObject , VariableExpressionSegmentMatcher matcher ) {
262+ protected Object doExtractIndexedValue (TestContext testContext , Object parentObject , VariableExpressionSegmentMatcher matcher )
263+ throws SegmentEvaluationException {
264+
265+ if (!(parentObject instanceof Map <?, ?> map )) {
266+ throw new SegmentEvaluationException (
267+ String .format ("Expected Map for segment '%s' but was %s" ,
268+ matcher .getSegmentExpression (), parentObject == null ? "null" : parentObject .getClass ().getName ()),
269+ renderObjectMinimal (parentObject ));
270+ }
213271
214- Object matchedValue = null ;
215- if (parentObject instanceof Map <?, ?>) {
216- matchedValue = ((Map <?, ?>) parentObject ).get (matcher .getSegmentExpression ());
272+ String key = matcher .getSegmentExpression ();
273+ if (!map .containsKey (key )) {
274+ throw new SegmentEvaluationException (
275+ String .format ("Unknown key '%s' in Map" , key ),
276+ renderObjectMinimal (parentObject ));
217277 }
218- return matchedValue ;
278+
279+ // Value may legitimately be null—return it as-is.
280+ return map .get (key );
219281 }
220282
221283 @ Override
222284 public boolean canExtract (TestContext testContext , Object object , VariableExpressionSegmentMatcher matcher ) {
223285 return object instanceof Map ;
224286 }
225287 }
288+
226289}
0 commit comments