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&amp;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}