001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.httppart; 014 015import static java.util.Collections.*; 016import static org.apache.juneau.httppart.HttpPartSchema.Format.*; 017import static org.apache.juneau.httppart.HttpPartSchema.Type.*; 018import static org.apache.juneau.internal.StringUtils.*; 019 020import java.lang.annotation.*; 021import java.lang.reflect.*; 022import java.math.*; 023import java.util.*; 024import java.util.concurrent.atomic.*; 025import java.util.regex.*; 026 027import org.apache.juneau.*; 028import org.apache.juneau.http.annotation.*; 029import org.apache.juneau.internal.*; 030import org.apache.juneau.parser.*; 031import org.apache.juneau.reflect.*; 032import org.apache.juneau.utils.*; 033 034/** 035 * Represents an OpenAPI schema definition. 036 * 037 * <p> 038 * The schema definition can be applied to any HTTP parts such as bodies, headers, query/form parameters, and URL path parts. 039 * <br>The API is generic enough to apply to any path part although some attributes may only applicable for certain parts. 040 * 041 * <p> 042 * Schema objects are created via builders instantiated through the {@link #create()} method. 043 * 044 * <p> 045 * This class is thread safe and reusable. 046 * 047 * <ul class='seealso'> 048 * <li class='link'>{@doc juneau-marshall.OpenApiDetails} 049 * </ul> 050 */ 051public class HttpPartSchema { 052 053 //------------------------------------------------------------------------------------------------------------------- 054 // Predefined instances 055 //------------------------------------------------------------------------------------------------------------------- 056 057 /** Reusable instance of this object, all default settings. */ 058 public static final HttpPartSchema DEFAULT = HttpPartSchema.create().allowEmptyValue(true).build(); 059 060 final String name; 061 final Set<Integer> codes; 062 final String _default; 063 final Set<String> _enum; 064 final Map<String,HttpPartSchema> properties; 065 final boolean allowEmptyValue, exclusiveMaximum, exclusiveMinimum, required, uniqueItems, skipIfEmpty; 066 final CollectionFormat collectionFormat; 067 final Type type; 068 final Format format; 069 final Pattern pattern; 070 final HttpPartSchema items, additionalProperties; 071 final Number maximum, minimum, multipleOf; 072 final Long maxLength, minLength, maxItems, minItems, maxProperties, minProperties; 073 final Class<? extends HttpPartParser> parser; 074 final Class<? extends HttpPartSerializer> serializer; 075 final ClassMeta<?> parsedType; 076 077 /** 078 * Instantiates a new builder for this object. 079 * 080 * @return A new builder for this object. 081 */ 082 public static HttpPartSchemaBuilder create() { 083 return new HttpPartSchemaBuilder(); 084 } 085 086 /** 087 * Finds the schema information for the specified method parameter. 088 * 089 * <p> 090 * This method will gather all the schema information from the annotations at the following locations: 091 * <ul> 092 * <li>The method parameter. 093 * <li>The method parameter class. 094 * <li>The method parameter parent classes and interfaces. 095 * </ul> 096 * 097 * @param c 098 * The annotation to look for. 099 * <br>Valid values: 100 * <ul> 101 * <li>{@link Body} 102 * <li>{@link Header} 103 * <li>{@link Query} 104 * <li>{@link FormData} 105 * <li>{@link Path} 106 * <li>{@link Response} 107 * <li>{@link ResponseHeader} 108 * <li>{@link ResponseBody} 109 * <li>{@link HasQuery} 110 * <li>{@link HasFormData} 111 * </ul> 112 * @param mpi The Java method parameter. 113 * @return The schema information about the parameter. 114 */ 115 public static HttpPartSchema create(Class<? extends Annotation> c, ParamInfo mpi) { 116 return create().apply(c, mpi).build(); 117 } 118 119 /** 120 * Finds the schema information for the specified method return. 121 * 122 * <p> 123 * This method will gather all the schema information from the annotations at the following locations: 124 * <ul> 125 * <li>The method. 126 * <li>The method return class. 127 * <li>The method return parent classes and interfaces. 128 * </ul> 129 * 130 * @param c 131 * The annotation to look for. 132 * <br>Valid values: 133 * <ul> 134 * <li>{@link Body} 135 * <li>{@link Header} 136 * <li>{@link Query} 137 * <li>{@link FormData} 138 * <li>{@link Path} 139 * <li>{@link Response} 140 * <li>{@link ResponseHeader} 141 * <li>{@link HasQuery} 142 * <li>{@link HasFormData} 143 * </ul> 144 * @param m 145 * The Java method with the return type being checked. 146 * @return The schema information about the parameter. 147 */ 148 public static HttpPartSchema create(Class<? extends Annotation> c, Method m) { 149 return create().apply(c, m).build(); 150 } 151 152 /** 153 * Finds the schema information for the specified class. 154 * 155 * <p> 156 * This method will gather all the schema information from the annotations on the class and all parent classes/interfaces. 157 * 158 * @param c 159 * The annotation to look for. 160 * <br>Valid values: 161 * <ul> 162 * <li>{@link Body} 163 * <li>{@link Header} 164 * <li>{@link Query} 165 * <li>{@link FormData} 166 * <li>{@link Path} 167 * <li>{@link Response} 168 * <li>{@link ResponseHeader} 169 * <li>{@link HasQuery} 170 * <li>{@link HasFormData} 171 * </ul> 172 * @param t 173 * The class containing the parameter. 174 * @return The schema information about the parameter. 175 */ 176 public static HttpPartSchema create(Class<? extends Annotation> c, java.lang.reflect.Type t) { 177 return create().apply(c, t).build(); 178 } 179 180 /** 181 * Shortcut for calling <c>create().type(type);</c> 182 * 183 * @param type The schema type value. 184 * @return A new builder. 185 */ 186 public static HttpPartSchemaBuilder create(String type) { 187 return create().type(type); 188 } 189 190 /** 191 * Shortcut for calling <c>create().type(type).format(format);</c> 192 * 193 * @param type The schema type value. 194 * @param format The schema format value. 195 * @return A new builder. 196 */ 197 public static HttpPartSchemaBuilder create(String type, String format) { 198 return create().type(type).format(format); 199 } 200 201 /** 202 * Finds the schema information on the specified annotation. 203 * 204 * @param a 205 * The annotation to find the schema information on.. 206 * @return The schema information found on the annotation. 207 */ 208 public static HttpPartSchema create(Annotation a) { 209 return create().apply(a).build(); 210 } 211 212 /** 213 * Finds the schema information on the specified annotation. 214 * 215 * @param a 216 * The annotation to find the schema information on.. 217 * @param defaultName The default part name if not specified on the annotation. 218 * @return The schema information found on the annotation. 219 */ 220 public static HttpPartSchema create(Annotation a, String defaultName) { 221 return create().name(defaultName).apply(a).build(); 222 } 223 224 HttpPartSchema(HttpPartSchemaBuilder b) { 225 this.name = b.name; 226 this.codes = copy(b.codes); 227 this._default = b._default; 228 this._enum = copy(b._enum); 229 this.properties = build(b.properties, b.noValidate); 230 this.allowEmptyValue = resolve(b.allowEmptyValue); 231 this.exclusiveMaximum = resolve(b.exclusiveMaximum); 232 this.exclusiveMinimum = resolve(b.exclusiveMinimum); 233 this.required = resolve(b.required); 234 this.uniqueItems = resolve(b.uniqueItems); 235 this.skipIfEmpty = resolve(b.skipIfEmpty); 236 this.collectionFormat = b.collectionFormat; 237 this.type = b.type; 238 this.format = b.format; 239 this.pattern = b.pattern; 240 this.items = build(b.items, b.noValidate); 241 this.additionalProperties = build(b.additionalProperties, b.noValidate); 242 this.maximum = b.maximum; 243 this.minimum = b.minimum; 244 this.multipleOf = b.multipleOf; 245 this.maxItems = b.maxItems; 246 this.maxLength = b.maxLength; 247 this.maxProperties = b.maxProperties; 248 this.minItems = b.minItems; 249 this.minLength = b.minLength; 250 this.minProperties = b.minProperties; 251 this.parser = b.parser; 252 this.serializer = b.serializer; 253 254 // Calculate parse type 255 Class<?> parsedType = Object.class; 256 if (type == ARRAY) { 257 if (items != null) 258 parsedType = Array.newInstance(items.parsedType.getInnerClass(), 0).getClass(); 259 } else if (type == BOOLEAN) { 260 parsedType = Boolean.class; 261 } else if (type == INTEGER) { 262 if (format == INT64) 263 parsedType = Long.class; 264 else 265 parsedType = Integer.class; 266 } else if (type == NUMBER) { 267 if (format == DOUBLE) 268 parsedType = Double.class; 269 else 270 parsedType = Float.class; 271 } else if (type == STRING) { 272 if (format == BYTE || format == BINARY || format == BINARY_SPACED) 273 parsedType = byte[].class; 274 else if (format == DATE || format == DATE_TIME) 275 parsedType = Calendar.class; 276 else 277 parsedType = String.class; 278 } 279 this.parsedType = BeanContext.DEFAULT.getClassMeta(parsedType); 280 281 if (b.noValidate) 282 return; 283 284 // Validation. 285 List<String> errors = new ArrayList<>(); 286 AList<String> notAllowed = new AList<>(); 287 boolean invalidFormat = false; 288 switch (type) { 289 case STRING: { 290 notAllowed.appendIf(properties != null, "properties"); 291 notAllowed.appendIf(additionalProperties != null, "additionalProperties"); 292 notAllowed.appendIf(exclusiveMaximum, "exclusiveMaximum"); 293 notAllowed.appendIf(exclusiveMinimum, "exclusiveMinimum"); 294 notAllowed.appendIf(uniqueItems, "uniqueItems"); 295 notAllowed.appendIf(collectionFormat != CollectionFormat.NO_COLLECTION_FORMAT, "collectionFormat"); 296 notAllowed.appendIf(items != null, "items"); 297 notAllowed.appendIf(maximum != null, "maximum"); 298 notAllowed.appendIf(minimum != null, "minimum"); 299 notAllowed.appendIf(multipleOf != null, "multipleOf"); 300 notAllowed.appendIf(maxItems != null, "maxItems"); 301 notAllowed.appendIf(minItems != null, "minItems"); 302 notAllowed.appendIf(minProperties != null, "minProperties"); 303 invalidFormat = ! format.isOneOf(Format.BYTE, Format.BINARY, Format.BINARY_SPACED, Format.DATE, Format.DATE_TIME, Format.PASSWORD, Format.UON, Format.NO_FORMAT); 304 break; 305 } 306 case ARRAY: { 307 notAllowed.appendIf(properties != null, "properties"); 308 notAllowed.appendIf(additionalProperties != null, "additionalProperties"); 309 notAllowed.appendIf(exclusiveMaximum, "exclusiveMaximum"); 310 notAllowed.appendIf(exclusiveMinimum, "exclusiveMinimum"); 311 notAllowed.appendIf(pattern != null, "pattern"); 312 notAllowed.appendIf(maximum != null, "maximum"); 313 notAllowed.appendIf(minimum != null, "minimum"); 314 notAllowed.appendIf(multipleOf != null, "multipleOf"); 315 notAllowed.appendIf(maxLength != null, "maxLength"); 316 notAllowed.appendIf(minLength != null, "minLength"); 317 notAllowed.appendIf(maxProperties != null, "maxProperties"); 318 notAllowed.appendIf(minProperties != null, "minProperties"); 319 invalidFormat = ! format.isOneOf(Format.NO_FORMAT, Format.UON); 320 break; 321 } 322 case BOOLEAN: { 323 notAllowed.appendIf(! _enum.isEmpty(), "_enum"); 324 notAllowed.appendIf(properties != null, "properties"); 325 notAllowed.appendIf(additionalProperties != null, "additionalProperties"); 326 notAllowed.appendIf(exclusiveMaximum, "exclusiveMaximum"); 327 notAllowed.appendIf(exclusiveMinimum, "exclusiveMinimum"); 328 notAllowed.appendIf(uniqueItems, "uniqueItems"); 329 notAllowed.appendIf(collectionFormat != CollectionFormat.NO_COLLECTION_FORMAT, "collectionFormat"); 330 notAllowed.appendIf(pattern != null, "pattern"); 331 notAllowed.appendIf(items != null, "items"); 332 notAllowed.appendIf(maximum != null, "maximum"); 333 notAllowed.appendIf(minimum != null, "minimum"); 334 notAllowed.appendIf(multipleOf != null, "multipleOf"); 335 notAllowed.appendIf(maxItems != null, "maxItems"); 336 notAllowed.appendIf(maxLength != null, "maxLength"); 337 notAllowed.appendIf(maxProperties != null, "maxProperties"); 338 notAllowed.appendIf(minItems != null, "minItems"); 339 notAllowed.appendIf(minLength != null, "minLength"); 340 notAllowed.appendIf(minProperties != null, "minProperties"); 341 invalidFormat = ! format.isOneOf(Format.NO_FORMAT, Format.UON); 342 break; 343 } 344 case FILE: { 345 break; 346 } 347 case INTEGER: { 348 notAllowed.appendIf(properties != null, "properties"); 349 notAllowed.appendIf(additionalProperties != null, "additionalProperties"); 350 notAllowed.appendIf(uniqueItems, "uniqueItems"); 351 notAllowed.appendIf(collectionFormat != CollectionFormat.NO_COLLECTION_FORMAT, "collectionFormat"); 352 notAllowed.appendIf(pattern != null, "pattern"); 353 notAllowed.appendIf(items != null, "items"); 354 notAllowed.appendIf(maxItems != null, "maxItems"); 355 notAllowed.appendIf(maxLength != null, "maxLength"); 356 notAllowed.appendIf(maxProperties != null, "maxProperties"); 357 notAllowed.appendIf(minItems != null, "minItems"); 358 notAllowed.appendIf(minLength != null, "minLength"); 359 notAllowed.appendIf(minProperties != null, "minProperties"); 360 invalidFormat = ! format.isOneOf(Format.NO_FORMAT, Format.UON, Format.INT32, Format.INT64); 361 break; 362 } 363 case NUMBER: { 364 notAllowed.appendIf(properties != null, "properties"); 365 notAllowed.appendIf(additionalProperties != null, "additionalProperties"); 366 notAllowed.appendIf(uniqueItems, "uniqueItems"); 367 notAllowed.appendIf(collectionFormat != CollectionFormat.NO_COLLECTION_FORMAT, "collectionFormat"); 368 notAllowed.appendIf(pattern != null, "pattern"); 369 notAllowed.appendIf(items != null, "items"); 370 notAllowed.appendIf(maxItems != null, "maxItems"); 371 notAllowed.appendIf(maxLength != null, "maxLength"); 372 notAllowed.appendIf(maxProperties != null, "maxProperties"); 373 notAllowed.appendIf(minItems != null, "minItems"); 374 notAllowed.appendIf(minLength != null, "minLength"); 375 notAllowed.appendIf(minProperties != null, "minProperties"); 376 invalidFormat = ! format.isOneOf(Format.NO_FORMAT, Format.UON, Format.FLOAT, Format.DOUBLE); 377 break; 378 } 379 case OBJECT: { 380 notAllowed.appendIf(exclusiveMaximum, "exclusiveMaximum"); 381 notAllowed.appendIf(exclusiveMinimum, "exclusiveMinimum"); 382 notAllowed.appendIf(uniqueItems, "uniqueItems"); 383 notAllowed.appendIf(collectionFormat != CollectionFormat.NO_COLLECTION_FORMAT, "collectionFormat"); 384 notAllowed.appendIf(pattern != null, "pattern"); 385 notAllowed.appendIf(items != null, "items"); 386 notAllowed.appendIf(maximum != null, "maximum"); 387 notAllowed.appendIf(minimum != null, "minimum"); 388 notAllowed.appendIf(multipleOf != null, "multipleOf"); 389 notAllowed.appendIf(maxItems != null, "maxItems"); 390 notAllowed.appendIf(maxLength != null, "maxLength"); 391 notAllowed.appendIf(minItems != null, "minItems"); 392 notAllowed.appendIf(minLength != null, "minLength"); 393 invalidFormat = ! format.isOneOf(Format.NO_FORMAT, Format.UON); 394 break; 395 } 396 default: 397 break; 398 } 399 400 if (! notAllowed.isEmpty()) 401 errors.add("Attributes not allow for type='"+type+"': " + StringUtils.join(notAllowed, ",")); 402 if (invalidFormat) 403 errors.add("Invalid format for type='"+type+"': '"+format+"'"); 404 if (exclusiveMaximum && maximum == null) 405 errors.add("Cannot specify exclusiveMaximum with maximum."); 406 if (exclusiveMinimum && minimum == null) 407 errors.add("Cannot specify exclusiveMinimum with minimum."); 408 if (required && _default != null) 409 errors.add("Cannot specify a default value on a required value."); 410 if (minLength != null && maxLength != null && maxLength < minLength) 411 errors.add("maxLength cannot be less than minLength."); 412 if (minimum != null && maximum != null && maximum.doubleValue() < minimum.doubleValue()) 413 errors.add("maximum cannot be less than minimum."); 414 if (minItems != null && maxItems != null && maxItems < minItems) 415 errors.add("maxItems cannot be less than minItems."); 416 if (minProperties != null && maxProperties != null && maxProperties < minProperties) 417 errors.add("maxProperties cannot be less than minProperties."); 418 if (minLength != null && minLength < 0) 419 errors.add("minLength cannot be less than zero."); 420 if (maxLength != null && maxLength < 0) 421 errors.add("maxLength cannot be less than zero."); 422 if (minItems != null && minItems < 0) 423 errors.add("minItems cannot be less than zero."); 424 if (maxItems != null && maxItems < 0) 425 errors.add("maxItems cannot be less than zero."); 426 if (minProperties != null && minProperties < 0) 427 errors.add("minProperties cannot be less than zero."); 428 if (maxProperties != null && maxProperties < 0) 429 errors.add("maxProperties cannot be less than zero."); 430 if (type == ARRAY && items != null && items.getType() == OBJECT && (format != UON && format != Format.NO_FORMAT)) 431 errors.add("Cannot define an array of objects unless array format is 'uon'."); 432 433 if (! errors.isEmpty()) 434 throw new ContextRuntimeException("Schema specification errors: \n\t" + join(errors, "\n\t"), new Object[0]); 435 } 436 437 /** 438 * Valid values for the <c>collectionFormat</c> field. 439 */ 440 public static enum CollectionFormat { 441 442 /** 443 * Comma-separated values (e.g. <js>"foo,bar"</js>). 444 */ 445 CSV, 446 447 /** 448 * Space-separated values (e.g. <js>"foo bar"</js>). 449 */ 450 SSV, 451 452 /** 453 * Tab-separated values (e.g. <js>"foo\tbar"</js>). 454 */ 455 TSV, 456 457 /** 458 * Pipe-separated values (e.g. <js>"foo|bar"</js>). 459 */ 460 PIPES, 461 462 /** 463 * Corresponds to multiple parameter instances instead of multiple values for a single instance (e.g. <js>"foo=bar&foo=baz"</js>). 464 */ 465 MULTI, 466 467 /** 468 * UON notation (e.g. <js>"@(foo,bar)"</js>). 469 */ 470 UON, 471 472 /** 473 * Not specified. 474 */ 475 NO_COLLECTION_FORMAT; 476 477 static CollectionFormat fromString(String value) { 478 479 return valueOf(value.toUpperCase()); 480 } 481 482 @Override 483 public String toString() { 484 return name().toLowerCase(); 485 } 486 } 487 488 /** 489 * Valid values for the <c>type</c> field. 490 */ 491 public static enum Type { 492 493 /** 494 * String. 495 */ 496 STRING, 497 498 /** 499 * Floating point number. 500 */ 501 NUMBER, 502 503 /** 504 * Decimal number. 505 */ 506 INTEGER, 507 508 /** 509 * Boolean. 510 */ 511 BOOLEAN, 512 513 /** 514 * Array or collection. 515 */ 516 ARRAY, 517 518 /** 519 * Map or bean. 520 */ 521 OBJECT, 522 523 /** 524 * File. 525 */ 526 FILE, 527 528 /** 529 * Not specified. 530 */ 531 NO_TYPE; 532 533 static Type fromString(String value) { 534 return valueOf(value.toUpperCase()); 535 } 536 537 @Override 538 public String toString() { 539 return name().toLowerCase(); 540 } 541 } 542 543 /** 544 * Valid values for the <c>format</c> field. 545 */ 546 public static enum Format { 547 548 /** 549 * Signed 32 bits. 550 */ 551 INT32, 552 553 /** 554 * Signed 64 bits. 555 */ 556 INT64, 557 558 /** 559 * 32-bit floating point number. 560 */ 561 FLOAT, 562 563 /** 564 * 64-bit floating point number. 565 */ 566 DOUBLE, 567 568 /** 569 * BASE-64 encoded characters. 570 */ 571 BYTE, 572 573 /** 574 * Hexadecimal encoded octets (e.g. <js>"00FF"</js>). 575 */ 576 BINARY, 577 578 /** 579 * Spaced-separated hexadecimal encoded octets (e.g. <js>"00 FF"</js>). 580 */ 581 BINARY_SPACED, 582 583 /** 584 * An <a href='http://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14'>RFC3339 full-date</a>. 585 */ 586 DATE, 587 588 /** 589 * An <a href='http://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14'>RFC3339 date-time</a>. 590 */ 591 DATE_TIME, 592 593 /** 594 * Used to hint UIs the input needs to be obscured. 595 */ 596 PASSWORD, 597 598 /** 599 * UON notation (e.g. <js>"(foo=bar,baz=@(qux,123))"</js>). 600 */ 601 UON, 602 603 /** 604 * Not specified. 605 */ 606 NO_FORMAT; 607 608 static Format fromString(String value) { 609 value = value.toUpperCase().replace('-','_'); 610 return valueOf(value); 611 } 612 613 @Override 614 public String toString() { 615 String s = name().toLowerCase().replace('_','-'); 616 return s; 617 } 618 619 /** 620 * Returns <jk>true</jk> if this format is in the provided list. 621 * 622 * @param list The list of formats to check against. 623 * @return <jk>true</jk> if this format is in the provided list. 624 */ 625 public boolean isOneOf(Format...list) { 626 for (Format ff : list) 627 if (this == ff) 628 return true; 629 return false; 630 } 631 } 632 633 /** 634 * Returns the default parsed type for this schema. 635 * 636 * @return The default parsed type for this schema. Never <jk>null</jk>. 637 */ 638 public ClassMeta<?> getParsedType() { 639 return parsedType; 640 } 641 642 /** 643 * Returns the name of the object described by this schema, for example the query or form parameter name. 644 * 645 * @return The name, or <jk>null</jk> if not specified. 646 * @see HttpPartSchemaBuilder#name(String) 647 */ 648 public String getName() { 649 return name; 650 } 651 652 /** 653 * Returns the HTTP status code or codes defined on a schema. 654 * 655 * @return 656 * The list of HTTP status codes. 657 * <br>Never <jk>null</jk>. 658 * @see HttpPartSchemaBuilder#code(int) 659 * @see HttpPartSchemaBuilder#codes(int[]) 660 */ 661 public Set<Integer> getCodes() { 662 return codes; 663 } 664 665 /** 666 * Returns the HTTP status code or codes defined on a schema. 667 * 668 * @param def The default value if there are no codes defined. 669 * @return 670 * The list of HTTP status codes. 671 * <br>A singleton set containing the default value if the set is empty. 672 * <br>Never <jk>null</jk>. 673 * @see HttpPartSchemaBuilder#code(int) 674 * @see HttpPartSchemaBuilder#codes(int[]) 675 */ 676 public Set<Integer> getCodes(Integer def) { 677 return codes.isEmpty() ? Collections.singleton(def) : codes; 678 } 679 680 /** 681 * Returns the first HTTP status code on a schema. 682 * 683 * @param def The default value if there are no codes defined. 684 * @return 685 * The list of HTTP status codes. 686 * <br>A singleton set containing the default value if the set is empty. 687 * <br>Never <jk>null</jk>. 688 * @see HttpPartSchemaBuilder#code(int) 689 * @see HttpPartSchemaBuilder#codes(int[]) 690 */ 691 public Integer getCode(Integer def) { 692 return codes.isEmpty() ? def : codes.iterator().next(); 693 } 694 695 /** 696 * Returns the <c>type</c> field of this schema. 697 * 698 * @return The <c>type</c> field of this schema, or <jk>null</jk> if not specified. 699 * @see HttpPartSchemaBuilder#type(String) 700 */ 701 public Type getType() { 702 return type; 703 } 704 705 /** 706 * Returns the <c>type</c> field of this schema. 707 * 708 * @param cm 709 * The class meta of the object. 710 * <br>Used to auto-detect the type if the type was not specified. 711 * @return The format field of this schema, or <jk>null</jk> if not specified. 712 * @see HttpPartSchemaBuilder#format(String) 713 */ 714 public Type getType(ClassMeta<?> cm) { 715 if (type != Type.NO_TYPE) 716 return type; 717 if (cm.isMapOrBean()) 718 return Type.OBJECT; 719 if (cm.isCollectionOrArray()) 720 return Type.ARRAY; 721 if (cm.isNumber()) { 722 if (cm.isDecimal()) 723 return Type.NUMBER; 724 return Type.INTEGER; 725 } 726 if (cm.isBoolean()) 727 return Type.BOOLEAN; 728 return Type.STRING; 729 } 730 731 /** 732 * Returns the <c>default</c> field of this schema. 733 * 734 * @return The default value for this schema, or <jk>null</jk> if not specified. 735 * @see HttpPartSchemaBuilder#_default(String) 736 */ 737 public String getDefault() { 738 return _default; 739 } 740 741 /** 742 * Returns the <c>collectionFormat</c> field of this schema. 743 * 744 * @return The <c>collectionFormat</c> field of this schema, or <jk>null</jk> if not specified. 745 * @see HttpPartSchemaBuilder#collectionFormat(String) 746 */ 747 public CollectionFormat getCollectionFormat() { 748 return collectionFormat; 749 } 750 751 /** 752 * Returns the <c>format</c> field of this schema. 753 * 754 * @see HttpPartSchemaBuilder#format(String) 755 * @return The <c>format</c> field of this schema, or <jk>null</jk> if not specified. 756 */ 757 public Format getFormat() { 758 return format; 759 } 760 761 /** 762 * Returns the <c>format</c> field of this schema. 763 * 764 * @param cm 765 * The class meta of the object. 766 * <br>Used to auto-detect the format if the format was not specified. 767 * @return The <c>format</c> field of this schema, or <jk>null</jk> if not specified. 768 * @see HttpPartSchemaBuilder#format(String) 769 */ 770 public Format getFormat(ClassMeta<?> cm) { 771 if (format != Format.NO_FORMAT) 772 return format; 773 if (cm.isNumber()) { 774 if (cm.isDecimal()) { 775 if (cm.isDouble()) 776 return Format.DOUBLE; 777 return Format.FLOAT; 778 } 779 if (cm.isLong()) 780 return Format.INT64; 781 return Format.INT32; 782 } 783 return format; 784 } 785 786 /** 787 * Returns the <c>maximum</c> field of this schema. 788 * 789 * @return The schema for child items of the object represented by this schema, or <jk>null</jk> if not defined. 790 * @see HttpPartSchemaBuilder#items(HttpPartSchemaBuilder) 791 */ 792 public HttpPartSchema getItems() { 793 return items; 794 } 795 796 /** 797 * Returns the <c>maximum</c> field of this schema. 798 * 799 * @return The <c>maximum</c> field of this schema, or <jk>null</jk> if not specified. 800 * @see HttpPartSchemaBuilder#maximum(Number) 801 */ 802 public Number getMaximum() { 803 return maximum; 804 } 805 806 /** 807 * Returns the <c>minimum</c> field of this schema. 808 * 809 * @return The <c>minimum</c> field of this schema, or <jk>null</jk> if not specified. 810 * @see HttpPartSchemaBuilder#minimum(Number) 811 */ 812 public Number getMinimum() { 813 return minimum; 814 } 815 816 /** 817 * Returns the <c>xxx</c> field of this schema. 818 * 819 * @return The <c>xxx</c> field of this schema, or <jk>null</jk> if not specified. 820 * @see HttpPartSchemaBuilder#multipleOf(Number) 821 */ 822 public Number getMultipleOf() { 823 return multipleOf; 824 } 825 826 /** 827 * Returns the <c>xxx</c> field of this schema. 828 * 829 * @return The <c>xxx</c> field of this schema, or <jk>null</jk> if not specified. 830 * @see HttpPartSchemaBuilder#pattern(String) 831 */ 832 public Pattern getPattern() { 833 return pattern; 834 } 835 836 /** 837 * Returns the <c>xxx</c> field of this schema. 838 * 839 * @return The <c>xxx</c> field of this schema, or <jk>null</jk> if not specified. 840 * @see HttpPartSchemaBuilder#maxLength(Long) 841 */ 842 public Long getMaxLength() { 843 return maxLength; 844 } 845 846 /** 847 * Returns the <c>xxx</c> field of this schema. 848 * 849 * @return The <c>xxx</c> field of this schema, or <jk>null</jk> if not specified. 850 * @see HttpPartSchemaBuilder#minLength(Long) 851 */ 852 public Long getMinLength() { 853 return minLength; 854 } 855 856 /** 857 * Returns the <c>xxx</c> field of this schema. 858 * 859 * @return The <c>xxx</c> field of this schema, or <jk>null</jk> if not specified. 860 * @see HttpPartSchemaBuilder#maxItems(Long) 861 */ 862 public Long getMaxItems() { 863 return maxItems; 864 } 865 866 /** 867 * Returns the <c>xxx</c> field of this schema. 868 * 869 * @return The <c>xxx</c> field of this schema, or <jk>null</jk> if not specified. 870 * @see HttpPartSchemaBuilder#minItems(Long) 871 */ 872 public Long getMinItems() { 873 return minItems; 874 } 875 876 /** 877 * Returns the <c>xxx</c> field of this schema. 878 * 879 * @return The <c>xxx</c> field of this schema, or <jk>null</jk> if not specified. 880 * @see HttpPartSchemaBuilder#maxProperties(Long) 881 */ 882 public Long getMaxProperties() { 883 return maxProperties; 884 } 885 886 /** 887 * Returns the <c>xxx</c> field of this schema. 888 * 889 * @return The <c>xxx</c> field of this schema, or <jk>null</jk> if not specified. 890 * @see HttpPartSchemaBuilder#minProperties(Long) 891 */ 892 public Long getMinProperties() { 893 return minProperties; 894 } 895 896 /** 897 * Returns the <c>exclusiveMaximum</c> field of this schema. 898 * 899 * @return The <c>exclusiveMaximum</c> field of this schema. 900 * @see HttpPartSchemaBuilder#exclusiveMaximum(Boolean) 901 */ 902 public boolean isExclusiveMaximum() { 903 return exclusiveMaximum; 904 } 905 906 /** 907 * Returns the <c>exclusiveMinimum</c> field of this schema. 908 * 909 * @return The <c>exclusiveMinimum</c> field of this schema. 910 * @see HttpPartSchemaBuilder#exclusiveMinimum(Boolean) 911 */ 912 public boolean isExclusiveMinimum() { 913 return exclusiveMinimum; 914 } 915 916 /** 917 * Returns the <c>uniqueItems</c> field of this schema. 918 * 919 * @return The <c>uniqueItems</c> field of this schema. 920 * @see HttpPartSchemaBuilder#uniqueItems(Boolean) 921 */ 922 public boolean isUniqueItems() { 923 return uniqueItems; 924 } 925 926 /** 927 * Returns the <c>required</c> field of this schema. 928 * 929 * @return The <c>required</c> field of this schema. 930 * @see HttpPartSchemaBuilder#required(Boolean) 931 */ 932 public boolean isRequired() { 933 return required; 934 } 935 936 /** 937 * Returns the <c>skipIfEmpty</c> field of this schema. 938 * 939 * @return The <c>skipIfEmpty</c> field of this schema. 940 * @see HttpPartSchemaBuilder#skipIfEmpty(Boolean) 941 */ 942 public boolean isSkipIfEmpty() { 943 return skipIfEmpty; 944 } 945 946 /** 947 * Returns the <c>allowEmptyValue</c> field of this schema. 948 * 949 * @return The <c>skipIfEmpty</c> field of this schema. 950 * @see HttpPartSchemaBuilder#skipIfEmpty(Boolean) 951 */ 952 public boolean isAllowEmptyValue() { 953 return allowEmptyValue; 954 } 955 956 /** 957 * Returns the <c>enum</c> field of this schema. 958 * 959 * @return The <c>enum</c> field of this schema, or <jk>null</jk> if not specified. 960 * @see HttpPartSchemaBuilder#_enum(Set) 961 */ 962 public Set<String> getEnum() { 963 return _enum; 964 } 965 966 /** 967 * Returns the <c>parser</c> field of this schema. 968 * 969 * @return The <c>parser</c> field of this schema, or <jk>null</jk> if not specified. 970 * @see HttpPartSchemaBuilder#parser(Class) 971 */ 972 public Class<? extends HttpPartParser> getParser() { 973 return parser; 974 } 975 976 /** 977 * Returns the <c>serializer</c> field of this schema. 978 * 979 * @return The <c>serializer</c> field of this schema, or <jk>null</jk> if not specified. 980 * @see HttpPartSchemaBuilder#serializer(Class) 981 */ 982 public Class<? extends HttpPartSerializer> getSerializer() { 983 return serializer; 984 } 985 986 /** 987 * Throws a {@link ParseException} if the specified pre-parsed input does not validate against this schema. 988 * 989 * @param in The input. 990 * @return The same object passed in. 991 * @throws SchemaValidationException if the specified pre-parsed input does not validate against this schema. 992 */ 993 public String validateInput(String in) throws SchemaValidationException { 994 if (! isValidRequired(in)) 995 throw new SchemaValidationException("No value specified."); 996 if (in != null) { 997 if (! isValidAllowEmpty(in)) 998 throw new SchemaValidationException("Empty value not allowed."); 999 if (! isValidPattern(in)) 1000 throw new SchemaValidationException("Value does not match expected pattern. Must match pattern: {0}", pattern.pattern()); 1001 if (! isValidEnum(in)) 1002 throw new SchemaValidationException("Value does not match one of the expected values. Must be one of the following: {0}", _enum); 1003 if (! isValidMaxLength(in)) 1004 throw new SchemaValidationException("Maximum length of value exceeded."); 1005 if (! isValidMinLength(in)) 1006 throw new SchemaValidationException("Minimum length of value not met."); 1007 } 1008 return in; 1009 } 1010 1011 /** 1012 * Throws a {@link ParseException} if the specified parsed output does not validate against this schema. 1013 * 1014 * @param o The parsed output. 1015 * @param bc The bean context used to detect POJO types. 1016 * @return The same object passed in. 1017 * @throws SchemaValidationException if the specified parsed output does not validate against this schema. 1018 */ 1019 @SuppressWarnings("rawtypes") 1020 public <T> T validateOutput(T o, BeanContext bc) throws SchemaValidationException { 1021 if (o == null) { 1022 if (! isValidRequired(o)) 1023 throw new SchemaValidationException("Required value not provided."); 1024 return o; 1025 } 1026 ClassMeta<?> cm = bc.getClassMetaForObject(o); 1027 switch (getType(cm)) { 1028 case ARRAY: { 1029 if (cm.isArray()) { 1030 if (! isValidMinItems(o)) 1031 throw new SchemaValidationException("Minimum number of items not met."); 1032 if (! isValidMaxItems(o)) 1033 throw new SchemaValidationException("Maximum number of items exceeded."); 1034 if (! isValidUniqueItems(o)) 1035 throw new SchemaValidationException("Duplicate items not allowed."); 1036 HttpPartSchema items = getItems(); 1037 if (items != null) 1038 for (int i = 0; i < Array.getLength(o); i++) 1039 items.validateOutput(Array.get(o, i), bc); 1040 } else if (cm.isCollection()) { 1041 Collection<?> c = (Collection<?>)o; 1042 if (! isValidMinItems(c)) 1043 throw new SchemaValidationException("Minimum number of items not met."); 1044 if (! isValidMaxItems(c)) 1045 throw new SchemaValidationException("Maximum number of items exceeded."); 1046 if (! isValidUniqueItems(c)) 1047 throw new SchemaValidationException("Duplicate items not allowed."); 1048 HttpPartSchema items = getItems(); 1049 if (items != null) 1050 for (Object o2 : c) 1051 items.validateOutput(o2, bc); 1052 } 1053 break; 1054 } 1055 case INTEGER: { 1056 if (cm.isNumber()) { 1057 Number n = (Number)o; 1058 if (! isValidMinimum(n)) 1059 throw new SchemaValidationException("Minimum value not met."); 1060 if (! isValidMaximum(n)) 1061 throw new SchemaValidationException("Maximum value exceeded."); 1062 if (! isValidMultipleOf(n)) 1063 throw new SchemaValidationException("Multiple-of not met."); 1064 } 1065 break; 1066 } 1067 case NUMBER: { 1068 if (cm.isNumber()) { 1069 Number n = (Number)o; 1070 if (! isValidMinimum(n)) 1071 throw new SchemaValidationException("Minimum value not met."); 1072 if (! isValidMaximum(n)) 1073 throw new SchemaValidationException("Maximum value exceeded."); 1074 if (! isValidMultipleOf(n)) 1075 throw new SchemaValidationException("Multiple-of not met."); 1076 } 1077 break; 1078 } 1079 case OBJECT: { 1080 if (cm.isMapOrBean()) { 1081 Map<?,?> m = cm.isMap() ? (Map<?,?>)o : bc.createSession().toBeanMap(o); 1082 if (! isValidMinProperties(m)) 1083 throw new SchemaValidationException("Minimum number of properties not met."); 1084 if (! isValidMaxProperties(m)) 1085 throw new SchemaValidationException("Maximum number of properties exceeded."); 1086 for (Map.Entry e : m.entrySet()) { 1087 String key = e.getKey().toString(); 1088 HttpPartSchema s2 = getProperty(key); 1089 if (s2 != null) 1090 s2.validateOutput(e.getValue(), bc); 1091 } 1092 } else if (cm.isBean()) { 1093 1094 } 1095 break; 1096 } 1097 case BOOLEAN: 1098 case FILE: 1099 case STRING: 1100 case NO_TYPE: 1101 break; 1102 } 1103 return o; 1104 } 1105 1106 //----------------------------------------------------------------------------------------------------------------- 1107 // Helper methods. 1108 //----------------------------------------------------------------------------------------------------------------- 1109 1110 private boolean isValidRequired(Object x) { 1111 return x != null || ! required; 1112 } 1113 1114 private boolean isValidMinProperties(Map<?,?> x) { 1115 return minProperties == null || x.size() >= minProperties; 1116 } 1117 1118 private boolean isValidMaxProperties(Map<?,?> x) { 1119 return maxProperties == null || x.size() <= maxProperties; 1120 } 1121 1122 private boolean isValidMinimum(Number x) { 1123 if (x instanceof Integer || x instanceof AtomicInteger) 1124 return minimum == null || x.intValue() > minimum.intValue() || (x.intValue() == minimum.intValue() && (! exclusiveMinimum)); 1125 if (x instanceof Short || x instanceof Byte) 1126 return minimum == null || x.shortValue() > minimum.shortValue() || (x.intValue() == minimum.shortValue() && (! exclusiveMinimum)); 1127 if (x instanceof Long || x instanceof AtomicLong || x instanceof BigInteger) 1128 return minimum == null || x.longValue() > minimum.longValue() || (x.intValue() == minimum.longValue() && (! exclusiveMinimum)); 1129 if (x instanceof Float) 1130 return minimum == null || x.floatValue() > minimum.floatValue() || (x.floatValue() == minimum.floatValue() && (! exclusiveMinimum)); 1131 if (x instanceof Double || x instanceof BigDecimal) 1132 return minimum == null || x.doubleValue() > minimum.doubleValue() || (x.doubleValue() == minimum.doubleValue() && (! exclusiveMinimum)); 1133 return true; 1134 } 1135 1136 private boolean isValidMaximum(Number x) { 1137 if (x instanceof Integer || x instanceof AtomicInteger) 1138 return maximum == null || x.intValue() < maximum.intValue() || (x.intValue() == maximum.intValue() && (! exclusiveMaximum)); 1139 if (x instanceof Short || x instanceof Byte) 1140 return maximum == null || x.shortValue() < maximum.shortValue() || (x.intValue() == maximum.shortValue() && (! exclusiveMaximum)); 1141 if (x instanceof Long || x instanceof AtomicLong || x instanceof BigInteger) 1142 return maximum == null || x.longValue() < maximum.longValue() || (x.intValue() == maximum.longValue() && (! exclusiveMaximum)); 1143 if (x instanceof Float) 1144 return maximum == null || x.floatValue() < maximum.floatValue() || (x.floatValue() == maximum.floatValue() && (! exclusiveMaximum)); 1145 if (x instanceof Double || x instanceof BigDecimal) 1146 return maximum == null || x.doubleValue() < maximum.doubleValue() || (x.doubleValue() == maximum.doubleValue() && (! exclusiveMaximum)); 1147 return true; 1148 } 1149 1150 private boolean isValidMultipleOf(Number x) { 1151 if (x instanceof Integer || x instanceof AtomicInteger) 1152 return multipleOf == null || x.intValue() % multipleOf.intValue() == 0; 1153 if (x instanceof Short || x instanceof Byte) 1154 return multipleOf == null || x.shortValue() % multipleOf.shortValue() == 0; 1155 if (x instanceof Long || x instanceof AtomicLong || x instanceof BigInteger) 1156 return multipleOf == null || x.longValue() % multipleOf.longValue() == 0; 1157 if (x instanceof Float) 1158 return multipleOf == null || x.floatValue() % multipleOf.floatValue() == 0; 1159 if (x instanceof Double || x instanceof BigDecimal) 1160 return multipleOf == null || x.doubleValue() % multipleOf.doubleValue() == 0; 1161 return true; 1162 } 1163 1164 private boolean isValidAllowEmpty(String x) { 1165 return allowEmptyValue || isNotEmpty(x); 1166 } 1167 1168 private boolean isValidPattern(String x) { 1169 return pattern == null || pattern.matcher(x).matches(); 1170 } 1171 1172 private boolean isValidEnum(String x) { 1173 return _enum.isEmpty() || _enum.contains(x); 1174 } 1175 1176 private boolean isValidMinLength(String x) { 1177 return minLength == null || x.length() >= minLength; 1178 } 1179 1180 private boolean isValidMaxLength(String x) { 1181 return maxLength == null || x.length() <= maxLength; 1182 } 1183 1184 private boolean isValidMinItems(Object x) { 1185 return minItems == null || Array.getLength(x) >= minItems; 1186 } 1187 1188 private boolean isValidMaxItems(Object x) { 1189 return maxItems == null || Array.getLength(x) <= maxItems; 1190 } 1191 1192 private boolean isValidUniqueItems(Object x) { 1193 if (uniqueItems) { 1194 Set<Object> s = new HashSet<>(); 1195 for (int i = 0; i < Array.getLength(x); i++) { 1196 Object o = Array.get(x, i); 1197 if (! s.add(o)) 1198 return false; 1199 } 1200 } 1201 return true; 1202 } 1203 1204 private boolean isValidMinItems(Collection<?> x) { 1205 return minItems == null || x.size() >= minItems; 1206 } 1207 1208 private boolean isValidMaxItems(Collection<?> x) { 1209 return maxItems == null || x.size() <= maxItems; 1210 } 1211 1212 private boolean isValidUniqueItems(Collection<?> x) { 1213 if (uniqueItems && ! (x instanceof Set)) { 1214 Set<Object> s = new HashSet<>(); 1215 for (Object o : x) 1216 if (! s.add(o)) 1217 return false; 1218 } 1219 return true; 1220 } 1221 1222 /** 1223 * Returns the schema information for the specified property. 1224 * 1225 * @param name The property name. 1226 * @return The schema information for the specified property, or <jk>null</jk> if properties are not defined on this schema. 1227 */ 1228 public HttpPartSchema getProperty(String name) { 1229 if (properties != null) { 1230 HttpPartSchema schema = properties.get(name); 1231 if (schema != null) 1232 return schema; 1233 } 1234 return additionalProperties; 1235 } 1236 1237 /** 1238 * Returns <jk>true</jk> if this schema has properties associated with it. 1239 * 1240 * @return <jk>true</jk> if this schema has properties associated with it. 1241 */ 1242 public boolean hasProperties() { 1243 return properties != null || additionalProperties != null; 1244 } 1245 1246 private static <T> Set<T> copy(Set<T> in) { 1247 return in == null ? Collections.emptySet() : unmodifiableSet(new LinkedHashSet<>(in)); 1248 } 1249 1250 private static Map<String,HttpPartSchema> build(Map<String,HttpPartSchemaBuilder> in, boolean noValidate) { 1251 if (in == null) 1252 return null; 1253 Map<String,HttpPartSchema> m = new LinkedHashMap<>(); 1254 for (Map.Entry<String,HttpPartSchemaBuilder> e : in.entrySet()) 1255 m.put(e.getKey(), e.getValue().noValidate(noValidate).build()); 1256 return unmodifiableMap(m); 1257 } 1258 1259 private static HttpPartSchema build(HttpPartSchemaBuilder in, boolean noValidate) { 1260 return in == null ? null : in.noValidate(noValidate).build(); 1261 } 1262 1263 1264 //----------------------------------------------------------------------------------------------------------------- 1265 // Helper methods. 1266 //----------------------------------------------------------------------------------------------------------------- 1267 1268 private boolean resolve(Boolean b) { 1269 return b == null ? false : b; 1270 } 1271 1272 final static Set<String> toSet(String[] s) { 1273 return toSet(joinnl(s)); 1274 } 1275 1276 final static Set<String> toSet(String s) { 1277 if (isEmpty(s)) 1278 return null; 1279 Set<String> set = new ASet<>(); 1280 try { 1281 for (Object o : StringUtils.parseListOrCdl(s)) 1282 set.add(o.toString()); 1283 } catch (ParseException e) { 1284 throw new RuntimeException(e); 1285 } 1286 return set; 1287 } 1288 1289 final static Number toNumber(String s) { 1290 try { 1291 if (isNotEmpty(s)) 1292 return parseNumber(s, Number.class); 1293 return null; 1294 } catch (ParseException e) { 1295 throw new RuntimeException(e); 1296 } 1297 } 1298 1299 final static ObjectMap toObjectMap(String[] ss) { 1300 String s = joinnl(ss); 1301 if (s.isEmpty()) 1302 return null; 1303 if (! isObjectMap(s, true)) 1304 s = "{" + s + "}"; 1305 try { 1306 return new ObjectMap(s); 1307 } catch (ParseException e) { 1308 throw new RuntimeException(e); 1309 } 1310 } 1311 1312 @Override 1313 public String toString() { 1314 try { 1315 ObjectMap m = new ObjectMap() 1316 .appendSkipEmpty("name", name) 1317 .appendSkipEmpty("type", type) 1318 .appendSkipEmpty("format", format) 1319 .appendSkipEmpty("codes", codes) 1320 .appendSkipEmpty("default", _default) 1321 .appendSkipEmpty("enum", _enum) 1322 .appendSkipEmpty("properties", properties) 1323 .appendSkipFalse("allowEmptyValue", allowEmptyValue) 1324 .appendSkipFalse("exclusiveMaximum", exclusiveMaximum) 1325 .appendSkipFalse("exclusiveMinimum", exclusiveMinimum) 1326 .appendSkipFalse("required", required) 1327 .appendSkipFalse("uniqueItems", uniqueItems) 1328 .appendSkipFalse("skipIfEmpty", skipIfEmpty) 1329 .appendIf(collectionFormat != CollectionFormat.NO_COLLECTION_FORMAT, "collectionFormat", collectionFormat) 1330 .appendSkipEmpty("pattern", pattern) 1331 .appendSkipNull("items", items) 1332 .appendSkipNull("additionalProperties", additionalProperties) 1333 .appendSkipMinusOne("maximum", maximum) 1334 .appendSkipMinusOne("minimum", minimum) 1335 .appendSkipMinusOne("multipleOf", multipleOf) 1336 .appendSkipMinusOne("maxLength", maxLength) 1337 .appendSkipMinusOne("minLength", minLength) 1338 .appendSkipMinusOne("maxItems", maxItems) 1339 .appendSkipMinusOne("minItems", minItems) 1340 .appendSkipMinusOne("maxProperties", maxProperties) 1341 .appendSkipMinusOne("minProperties", minProperties) 1342 .append("parsedType", parsedType) 1343 ; 1344 return m.toString(); 1345 } catch (Exception e) { 1346 e.printStackTrace(); 1347 return ""; 1348 } 1349 } 1350}