001package Torello.JSON; 002 003import Torello.JavaDoc.Annotations.IntoHTMLTable; 004import static Torello.JavaDoc.Annotations.IntoHTMLTable.Background.BlueDither; 005import static Torello.JavaDoc.Annotations.IntoHTMLTable.Background.GreenDither; 006 007import static Torello.JSON.JFlag.*; 008 009import javax.json.*; 010 011import java.lang.reflect.Constructor; 012import java.lang.reflect.Modifier; 013 014import java.util.Objects; 015import java.util.function.Function; 016 017import static javax.json.JsonValue.ValueType.*; 018 019/** 020 * Builds on the J2EE Standard Release JSON Parsing Tools by providing additional 021 * help with converting JSON Data into Java Object-Data. 022 * 023 * <EMBED CLASS='external-html' DATA-FILE-ID=ALL_CLASSES_NOTE> 024 * <EMBED CLASS='external-html' DATA-FILE-ID=READ_JSON> 025 * <EMBED CLASS='external-html' DATA-FILE-ID=JO_DESERIALIZE> 026 * 027 * @see Json 028 * @see JsonObject 029 * @see JsonArray 030 */ 031@Torello.JavaDoc.Annotations.StaticFunctional 032public class ReadJSON 033{ 034 // This is a static class. Has no program state. 035 private ReadJSON() { } 036 037 038 // ******************************************************************************************** 039 // ******************************************************************************************** 040 // Object, Class<T> 041 // ******************************************************************************************** 042 // ******************************************************************************************** 043 044 045 /** 046 * Retrieve a {@link JsonArray} element, and transform it to a Java {@code Object} (POJO). 047 * <EMBED CLASS='external-HTML' DATA-FILE-ID=READ_POJO_NOTE> 048 * <EMBED CLASS=defs DATA-JTYPE=JsonObject DATA-TYPE='Type Parameter T'> 049 * 050 * @param <T> <EMBED CLASS='external-html' DATA-FILE-ID=READ_TYPE_PARAM_T> 051 * @param ja Any instance of {@link JsonArray} 052 * @param index A valid index into {@code 'ja'} 053 * @param c <EMBED CLASS='external-html' DATA-FILE-ID=READ_C> 054 * @param throwOnNull <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JA> 055 * @return <EMBED CLASS='external-html' DATA-FILE-ID=READJSON_RET_TON_JA> 056 * 057 * @throws IndexOutOfBoundsException If {@code 'index'} is out of bounds of {@code 'ja'} 058 * @throws NullPointerException If either {@code 'c'} or {@code 'ja'} are passed null 059 * @throws InvalidClassException <EMBED CLASS='external-html' DATA-FILE-ID=INV_CLASS_EX> 060 * @throws JsonTypeArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JTAEX> 061 * @throws JsonNullArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JNAEX> 062 * @throws JsonBuildPOJOArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JBPAEX> 063 */ 064 public static <T> T getObject( 065 final JsonArray ja, 066 final int index, 067 final Class<T> c, 068 final boolean throwOnNull 069 ) 070 { 071 final JsonValue jv = NPE_CHECK_JA(ja).get(index); 072 final Constructor<T> ctor = InvalidClassException.check(c); 073 074 switch (jv.getValueType()) 075 { 076 case NULL: 077 // This is simple-stuff (not rocket-science). "Type Mapping" Code has to worry 078 // about what the meaning of "null" should be. 079 080 if (throwOnNull) throw new JsonNullArrException(ja, index, OBJECT, c); 081 else return null; 082 083 case OBJECT: 084 try 085 { return ctor.newInstance((JsonObject) jv); } 086 087 catch (Exception e) 088 { throw new JsonBuildPOJOArrException(e, ja, index, (JsonObject) jv, c); } 089 090 // The JsonValue at the specified array-index does not contain a JsonObject. 091 default: throw new JsonTypeArrException(ja, index, OBJECT, jv, c); 092 } 093 } 094 095 /** 096 * Extract a {@link JsonObject} property, and transform it to a Java {@code Object} (POJO). 097 * <EMBED CLASS='external-HTML' DATA-FILE-ID=READ_POJO_NOTE> 098 * <EMBED CLASS=defs DATA-TYPE='Type Parameter T' DATA-JTYPE=JsonObject> 099 * 100 * @param <T> <EMBED CLASS='external-html' DATA-FILE-ID=READ_TYPE_PARAM_T> 101 * @param jo Any instance of {@link JsonObject}. 102 * @param propertyName Any property name contained by {@code 'jo'} 103 * @param isOptional <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_IS_OPTIONAL> 104 * @param throwOnNull <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JO> 105 * @return <EMBED CLASS='external-html' DATA-FILE-ID=READJSON_RET_TON_JO> 106 * 107 * @throws JsonPropMissingException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JPMEX> 108 * @throws NullPointerException If {@code 'jo', 'propertyName'} or {@code 'c'} are null 109 * @throws InvalidClassException <EMBED CLASS='external-html' DATA-FILE-ID=INV_CLASS_EX> 110 * @throws JsonTypeObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JTOEX> 111 * @throws JsonNullObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JNOEX> 112 * @throws JsonBuildPOJOObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JBPOEX> 113 */ 114 public static <T> T getObject( 115 final JsonObject jo, 116 final String propertyName, 117 final Class<T> c, 118 final boolean isOptional, 119 final boolean throwOnNull 120 ) 121 { 122 final JsonValue jv = NPE_CHECK_JO(jo, propertyName); 123 final Constructor<T> ctor = InvalidClassException.check(c); 124 125 if (jv == null) 126 { 127 if (isOptional) return null; 128 throw new JsonPropMissingException(jo, propertyName, OBJECT, c); 129 } 130 131 switch (jv.getValueType()) 132 { 133 case NULL: 134 135 // This is simple-stuff (not rocket-science). "Type Mapping" Code has to 136 // worry about what the meaning of "null" should be. 137 138 if (throwOnNull) throw new JsonNullObjException(jo, propertyName, OBJECT, c); 139 else return null; 140 141 case OBJECT: 142 try 143 { return ctor.newInstance(jo); } 144 145 catch (Exception e) 146 { 147 throw new JsonBuildPOJOObjException 148 (e, jo, propertyName, (JsonObject) jv, c); 149 } 150 151 // The JsonObject propertydoes not contain a JsonObject. 152 default: throw new JsonTypeObjException(jo, propertyName, OBJECT, jv, c); 153 } 154 } 155 156 157 // ******************************************************************************************** 158 // ******************************************************************************************** 159 // Object, Function<JsonObject, T> 160 // ******************************************************************************************** 161 // ******************************************************************************************** 162 163 164 /** <EMBED CLASS='external-html' DATA-FILE-ID=READJSON_GETOBJ_JA2> */ 165 @IntoHTMLTable( 166 title= 167 "Reads one JsonObject from a JsonArray, and uses a Generator Function to build " + 168 "a POJO", 169 background=BlueDither 170 ) 171 public static <T> T getObject( 172 final JsonArray ja, 173 final int index, 174 final Function<JsonObject, ? extends T> builderFunction, 175 final Class<T> c, 176 final boolean throwOnNull 177 ) 178 { 179 final JsonValue jv = NPE_CHECK_JA(ja).get(index); 180 181 switch (jv.getValueType()) 182 { 183 case NULL: 184 if (throwOnNull) throw new JsonNullArrException(ja, index, OBJECT, c); 185 else return null; 186 187 case OBJECT: 188 try 189 { return builderFunction.apply((JsonObject) jv); } 190 191 catch (Exception e) 192 { throw new JsonBuildPOJOArrException(e, ja, index, (JsonObject) jv, c); } 193 194 default: throw new JsonTypeArrException(ja, index, OBJECT, jv, c); 195 } 196 } 197 198 /** <EMBED CLASS='external-html' DATA-FILE-ID=READJSON_GETOBJ_JO2> */ 199 @IntoHTMLTable( 200 title= 201 "Reads one JsonObject from a JsonArray, and uses a Generator Function to build " + 202 "a POJO", 203 background=GreenDither 204 ) 205 public static <T> T getObject( 206 final JsonObject jo, 207 final String propertyName, 208 final Function<JsonObject, ? extends T> builderFunction, 209 final Class<T> c, 210 final boolean isOptional, 211 final boolean throwOnNull 212 ) 213 { 214 final JsonValue jv = NPE_CHECK_JO(jo, propertyName); 215 216 if (jv == null) 217 { 218 if (isOptional) return null; 219 throw new JsonPropMissingException(jo, propertyName, OBJECT, c); 220 } 221 222 switch (jv.getValueType()) 223 { 224 case NULL: 225 if (throwOnNull) throw new JsonNullObjException(jo, propertyName, OBJECT, c); 226 else return null; 227 228 case OBJECT: 229 try 230 { return builderFunction.apply((JsonObject) jv); } 231 232 catch (Exception e) 233 { 234 throw new JsonBuildPOJOObjException 235 (e, jo, propertyName, (JsonObject) jv, c); 236 } 237 238 default: throw new JsonTypeObjException(jo, propertyName, OBJECT, jv, c); 239 } 240 } 241 242 /** 243 * This class contains a lot of the reason / impetus for writing {@code 'ReadJSON'}. This 244 * does converts a {@link JsonObject} into a Java-Object. The actual binding must be 245 * implemented by the programmer - because the class-type that is passed to this method 246 * (parameter {@code 'c'}) must have a constructor accepting this {@code JsonObject}. 247 * 248 * <EMBED CLASS='external-html' DATA-FILE-ID=READ_POJO_NOTE> 249 * 250 * @param <T> <EMBED CLASS='external-html' DATA-FILE-ID=READ_TYPE_PARAM_T> 251 * @param jo This may be any {@link JsonObject} which can be bound to {@code 'c'}. 252 * @param c <EMBED CLASS='external-html' DATA-FILE-ID=READ_C> 253 * @return An instance of the class {@code 'T'}, which is specified by {@code 'c'} 254 * 255 * @throws JsonException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JEX_REFL> 256 * @throws NullPointerException If {@code 'jo'} or {@code 'c'} are passed null 257 * 258 * @throws InvalidClassException Throws if a valid, single-argument, {@code 'JsonObject'} 259 * constructor is not present or is not accessible within {@code 'c'} 260 */ 261 public static <T> T getObject(JsonObject jo, Class<T> c) 262 { 263 Objects.requireNonNull(jo, "You have passed null to Class Parameter 'jo'"); 264 Objects.requireNonNull(c, "You have passed null to Class Parameter 'c'"); 265 266 final Constructor<T> ctor = InvalidClassException.check(c); 267 268 try 269 { return ctor.newInstance(jo); } 270 271 catch (Exception e) 272 { 273 // *MANY* possible Exception's may be thrown, and *ALL* are checked exceptions 274 throw new JsonException( 275 "Unable to instantiate class: [" + c.getName() + "] using provided " + 276 "Java-Object\n" + 277 "See Exception.getCause() for details.", 278 e 279 ); 280 } 281 } 282 283 284 // ******************************************************************************************** 285 // ******************************************************************************************** 286 // String 287 // ******************************************************************************************** 288 // ******************************************************************************************** 289 290 291 /** 292 * Retrieve a {@link JsonArray} element, and transform it to a {@code java.lang.String}. 293 * <EMBED CLASS=defs DATA-JTYPE=JsonString DATA-TYPE='java.lang.String'> 294 * 295 * @param ja Any instance of {@link JsonArray} 296 * @param index A valid index into {@code 'ja'} 297 * @param throwOnNull <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JA> 298 * @return <EMBED CLASS='external-html' DATA-FILE-ID=READJSON_RET_TON_JA> 299 * 300 * @throws IndexOutOfBoundsException If {@code 'index'} is out of the bounds of {@code 'ja'} 301 * @throws JsonTypeArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JTAEX> 302 * @throws JsonNullArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JNAEX> 303 * @throws NullPointerException If {@code 'ja'} is passed null. 304 * 305 * @see JsonValue#getValueType() 306 * @see JsonValue.ValueType#STRING 307 */ 308 public static String getString(JsonArray ja, int index, boolean throwOnNull) 309 { 310 // This will also throw an IndexOutOfBoundsException if the index is out of bounds. 311 final JsonValue jv = NPE_CHECK_JA(ja).get(index); 312 313 switch (jv.getValueType()) 314 { 315 case NULL: 316 317 // This is simple-stuff (not rocket-science). "Type Mapping" Code has to worry 318 // about what the meaning of "null" should be. 319 320 if (throwOnNull) throw new JsonNullArrException(ja, index, STRING, String.class); 321 else return null; 322 323 case STRING: return ((JsonString) jv).getString(); 324 325 // The JsonValue at the specified array-index does not contain a JsonString. 326 default: throw new JsonTypeArrException(ja, index, STRING, jv, String.class); 327 } 328 } 329 330 /** 331 * Extract a {@link JsonObject} property, and transform it to a {@code java.lang.String}. 332 * <EMBED CLASS=defs DATA-TYPE='java.lang.String' DATA-JTYPE=JsonString> 333 * 334 * @param jo Any instance of {@link JsonObject}. 335 * @param propertyName Any property name contained by {@code 'jo'} 336 * @param isOptional <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_IS_OPTIONAL> 337 * @param throwOnNull <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JO> 338 * @return <EMBED CLASS='external-html' DATA-FILE-ID=READJSON_RET_TON_JO> 339 * 340 * @throws JsonPropMissingException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JPMEX> 341 * @throws JsonTypeObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JTOEX> 342 * @throws JsonNullObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JNOEX> 343 * @throws NullPointerException if either {@code 'jo'} or {@code 'propertyName'} are null 344 * 345 * @see JsonValue#getValueType() 346 * @see JsonValue.ValueType#STRING 347 */ 348 public static String getString 349 (JsonObject jo, String propertyName, boolean isOptional, boolean throwOnNull) 350 { 351 final JsonValue jv = NPE_CHECK_JO(jo, propertyName); 352 353 if (jv == null) 354 { 355 if (isOptional) return null; 356 throw new JsonPropMissingException(jo, propertyName, STRING, String.class); 357 } 358 359 switch (jv.getValueType()) 360 { 361 case NULL: 362 363 // This is simple-stuff (not rocket-science). "Type Mapping" Code has to worry 364 // about what the meaning of "null" should be. 365 366 if (throwOnNull) throw new JsonNullObjException 367 (jo, propertyName, STRING, String.class); 368 369 else return null; 370 371 case STRING: return ((JsonString) jv).getString(); 372 373 // The JsonObject propertydoes not contain a JsonString. 374 default: throw new JsonTypeObjException(jo, propertyName, STRING, jv, String.class); 375 } 376 } 377 378 379 // ******************************************************************************************** 380 // ******************************************************************************************** 381 // JsonObject 382 // ******************************************************************************************** 383 // ******************************************************************************************** 384 385 386 /** 387 * Extract an instance of {@link JsonObject} from an instance of {@link JsonArray}. 388 * 389 * <EMBED CLASS=defs DATA-JTYPE=JsonObject DATA-TYPE=JsonObject> 390 * @param ja Any instance of {@link JsonArray} 391 * @param index A valid index into {@code 'ja'} 392 * @param throwOnNull <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JA> 393 * 394 * @return The requested {@link JsonObject}, or null (only if null's have been permitted). 395 * @throws IndexOutOfBoundsException If {@code 'index'} is out of bounds of {@code 'ja'} 396 * @throws JsonTypeArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JTAEX> 397 * @throws JsonNullArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JNAEX> 398 * @throws NullPointerException If {@code 'ja'} is passed null 399 * 400 * @see JsonValue#getValueType() 401 * @see JsonValue.ValueType#OBJECT 402 */ 403 public static JsonObject getJsonObject(JsonArray ja, int index, boolean throwOnNull) 404 { 405 // This will also throw an IndexOutOfBoundsException if the index is out of bounds. 406 final JsonValue jv = NPE_CHECK_JA(ja).get(index); 407 408 switch (jv.getValueType()) 409 { 410 case NULL: 411 if (throwOnNull) 412 throw new JsonNullArrException(ja, index, OBJECT, JsonObject.class); 413 else 414 return null; 415 416 case OBJECT: return (JsonObject) jv; 417 418 // The JsonValue at the specified array-index does not contain a JsonObject. 419 default: throw new JsonTypeArrException(ja, index, OBJECT, jv, JsonObject.class); 420 } 421 } 422 423 /** 424 * Extract an instance of {@link JsonObject} from an instance of {@link JsonObject}. 425 * 426 * <EMBED CLASS=defs DATA-JTYPE=JsonObject DATA-TYPE=JsonObject> 427 * @param jo Any instance of {@link JsonObject}. 428 * @param propertyName Name of the JSON property that should be contained within {@code 'jo'} 429 * @param isOptional <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_IS_OPTIONAL> 430 * @param throwOnNull <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JO> 431 * 432 * @return The requested {@link JsonObject}, or null (only if null's have been permitted). 433 * @throws JsonPropMissingException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JPMEX> 434 * @throws JsonTypeObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JTOEX> 435 * @throws JsonNullObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JNOEX> 436 * @throws NullPointerException if either {@code 'jo'} or {@code 'propertyName'} are null 437 * 438 * @see JsonValue#getValueType() 439 * @see JsonValue.ValueType#OBJECT 440 */ 441 public static JsonObject getJsonObject 442 (JsonObject jo, String propertyName, boolean isOptional, boolean throwOnNull) 443 { 444 final JsonValue jv = NPE_CHECK_JO(jo, propertyName); 445 446 if (jv == null) 447 { 448 if (isOptional) return null; 449 throw new JsonPropMissingException(jo, propertyName, OBJECT, JsonObject.class); 450 } 451 452 switch (jv.getValueType()) 453 { 454 case NULL: 455 if (throwOnNull) 456 throw new JsonNullObjException(jo, propertyName, OBJECT, JsonObject.class); 457 else 458 return null; 459 460 case OBJECT: return (JsonObject) jv; 461 462 // The JsonValue at the specified property does not contain a JsonObject. 463 default: 464 throw new JsonTypeObjException(jo, propertyName, OBJECT, jv, JsonObject.class); 465 } 466 } 467 468 469 // ******************************************************************************************** 470 // ******************************************************************************************** 471 // JsonArray 472 // ******************************************************************************************** 473 // ******************************************************************************************** 474 475 476 /** 477 * Extract an instance of {@link JsonArray} from an instance of {@link JsonArray}. 478 * 479 * <EMBED CLASS=defs DATA-JTYPE=JsonArray DATA-TYPE=JsonArray> 480 * @param ja Any instance of {@link JsonArray} 481 * @param index A valid index into {@code 'ja'} 482 * @param throwOnNull <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JA> 483 * 484 * @return The requested {@link JsonArray}, or null (only if null's have been permitted). 485 * @throws IndexOutOfBoundsException If {@code 'index'} is out of bounds of {@code 'ja'} 486 * @throws JsonTypeArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JTAEX> 487 * @throws JsonNullArrException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JNAEX> 488 * @throws NullPointerException if {@code 'ja'} is passed null 489 * 490 * @see JsonValue#getValueType() 491 * @see JsonValue.ValueType#ARRAY 492 */ 493 public static JsonArray getJsonArray(JsonArray ja, int index, boolean throwOnNull) 494 { 495 // This will also throw an IndexOutOfBoundsException if the index is out of bounds. 496 final JsonValue jv = NPE_CHECK_JA(ja).get(index); 497 498 switch (jv.getValueType()) 499 { 500 case NULL: 501 if (throwOnNull) 502 throw new JsonNullArrException(ja, index, ARRAY, JsonArray.class); 503 else 504 return null; 505 506 case ARRAY: return (JsonArray) jv; 507 508 // The JsonValue at the specified property does not contain a JsonArray. 509 default: throw new JsonTypeArrException(ja, index, ARRAY, jv, JsonArray.class); 510 } 511 } 512 513 /** 514 * Extract an instance of {@link JsonArray} from an instance of {@link JsonObject}. 515 * 516 * <EMBED CLASS=defs DATA-JTYPE=JsonArray DATA-TYPE=JsonArray> 517 * @param jo Any instance of {@link JsonObject}. 518 * @param propertyName Name of the JSON property that should be contained within {@code 'jo'} 519 * @param isOptional <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_IS_OPTIONAL> 520 * @param throwOnNull <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JO> 521 * 522 * @return The requested {@link JsonArray}, or null (only if null's have been permitted). 523 * @throws JsonPropMissingException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JPMEX> 524 * @throws JsonTypeObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JTOEX> 525 * @throws JsonNullObjException <EMBED CLASS='external-html' DATA-FILE-ID=READ_JNOEX> 526 * @throws NullPointerException if either {@code 'jo'} or {@code 'propertyName'} are null 527 * 528 * @see JsonValue#getValueType() 529 * @see JsonValue.ValueType#ARRAY 530 */ 531 public static JsonArray getJsonArray 532 (JsonObject jo, String propertyName, boolean isOptional, boolean throwOnNull) 533 { 534 final JsonValue jv = NPE_CHECK_JO(jo, propertyName); 535 536 if (jv == null) 537 { 538 if (isOptional) return null; 539 throw new JsonPropMissingException(jo, propertyName, ARRAY, JsonArray.class); 540 } 541 542 switch (jv.getValueType()) 543 { 544 case NULL: 545 if (throwOnNull) 546 throw new JsonNullObjException(jo, propertyName, ARRAY, JsonArray.class); 547 else 548 return null; 549 550 case ARRAY: return (JsonArray) jv; 551 552 // The JsonValue at the specified property does not contain a JsonArray. 553 default: 554 throw new JsonTypeObjException(jo, propertyName, ARRAY, jv, JsonArray.class); 555 } 556 } 557 558 // ******************************************************************************************** 559 // ******************************************************************************************** 560 // Internal Methods 561 // ******************************************************************************************** 562 // ******************************************************************************************** 563 564 565 private static JsonArray NPE_CHECK_JA(JsonArray ja) 566 { 567 Objects.requireNonNull(ja, "You have passed null to JsonArray Parameter 'ja'"); 568 return ja; 569 } 570 571 private static JsonValue NPE_CHECK_JO(JsonObject jo, String property) 572 { 573 Objects.requireNonNull(jo, "You have passed null to JsonObject Parameter 'jo'"); 574 Objects.requireNonNull(property, "You have passed null to String Parameter 'property'"); 575 return jo.get(property); 576 } 577 578 private static JsonObject NPE_CHECK_JO(JsonObject jo) 579 { 580 Objects.requireNonNull(jo, "You have passed null to JsonObject Parameter 'jo'"); 581 return jo; 582 } 583}