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