// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.dto.jsonschema;

import static org.apache.juneau.common.internal.StringUtils.*;
import static org.apache.juneau.internal.ClassUtils.*;
import static org.apache.juneau.internal.CollectionUtils.*;

import java.net.*;
import java.util.*;

import org.apache.juneau.*;
import org.apache.juneau.annotation.*;
import org.apache.juneau.json.*;
import org.apache.juneau.parser.*;
import org.apache.juneau.serializer.*;
import org.apache.juneau.swap.*;

/**
 * Represents a top-level schema object bean in the JSON-Schema core specification.
 */
@Bean(typeName="schema",
	properties="id,$schema,$ref, title,description,type,definitions,properties,"
		+ "patternProperties,dependencies,items,multipleOf,maximum,exclusiveMaximum,"
		+ "minimum,exclusiveMinimum,maxLength,minLength,pattern,additionalItems,"
		+ "maxItems,minItems,uniqueItems,maxProperties,minProperties,required,"
		+ "additionalProperties,enum,allOf,anyOf,oneOf,not"
)
public class JsonSchema {
	private String name;                                   // Property name.  Not serialized.
	private URI id;
	private URI schemaVersion;
	private String title;
	private String description;
	private JsonType typeJsonType;                         // JsonType representation of type
	private JsonTypeArray typeJsonTypeArray;               // JsonTypeArray representation of type
	private Map<String,JsonSchema> definitions;
	private Map<String,JsonSchema> properties;
	private Map<String,JsonSchema> patternProperties;
	private Map<String,JsonSchema> dependencies;
	private JsonSchema itemsSchema;                            // JsonSchema representation of items
	private JsonSchemaArray itemsSchemaArray;                  // JsonSchemaArray representation of items
	private Number multipleOf;
	private Number maximum;
	private Boolean exclusiveMaximum;
	private Number minimum;
	private Boolean exclusiveMinimum;
	private Integer maxLength;
	private Integer minLength;
	private String pattern;
	private Boolean additionalItemsBoolean;                // Boolean representation of additionalItems
	private JsonSchemaArray additionalItemsSchemaArray;        // JsonSchemaArray representation of additionalItems
	private Integer maxItems;
	private Integer minItems;
	private Boolean uniqueItems;
	private Integer maxProperties;
	private Integer minProperties;
	private List<String> required;
	private Boolean additionalPropertiesBoolean;           // Boolean representation of additionalProperties
	private JsonSchema additionalPropertiesSchema;             // JsonSchema representation of additionalProperties
	private List<String> _enum;
	private List<JsonSchema> allOf;
	private List<JsonSchema> anyOf;
	private List<JsonSchema> oneOf;
	private JsonSchema not;
	private URI ref;
	private JsonSchemaMap schemaMap;
	private JsonSchema master = this;

	/**
	 * Default constructor.
	 */
	public JsonSchema() {}


	//-----------------------------------------------------------------------------------------------------------------
	// Bean properties
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Bean property getter:  <property>name</property>.
	 *
	 * @return The value of the <property>name</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public String getName() {
		return name;
	}

	/**
	 * Bean property setter:  <property>name</property>.
	 *
	 * @param name The new value for the <property>name</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setName(String name) {
		this.name = name;
		return this;
	}

	/**
	 * Bean property getter:  <property>id</property>.
	 *
	 * @return The value of the <property>id</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public URI getId() {
		return id;
	}

	/**
	 * Bean property setter:  <property>id</property>.
	 *
	 * <p>
	 * The value can be of any of the following types: {@link URI}, {@link URL}, {@link String}.
	 * Strings must be valid URIs.
	 *
	 * <p>
	 * URIs defined by {@link UriResolver} can be used for values.
	 *
	 * @param id The new value for the <property>id</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setId(Object id) {
		this.id = toURI(id);
		return this;
	}

	/**
	 * Bean property getter:  <property>$schema</property>.
	 *
	 * @return The value of the <property>$schema</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	@Beanp("$schema")
	public URI getSchemaVersionUri() {
		return schemaVersion;
	}

	/**
	 * Bean property setter:  <property>$schema</property>.
	 *
	 * <p>
	 * The value can be of any of the following types: {@link URI}, {@link URL}, {@link String}.
	 * Strings must be valid URIs.
	 *
	 * <p>
	 * URIs defined by {@link UriResolver} can be used for values.
	 *
	 * @param schemaVersion The new value for the <property>schemaVersion</property> property on this bean.
	 * @return This object.
	 */
	@Beanp("$schema")
	public JsonSchema setSchemaVersionUri(Object schemaVersion) {
		this.schemaVersion = toURI(schemaVersion);
		return this;
	}

	/**
	 * Bean property getter:  <property>title</property>.
	 *
	 * @return The value of the <property>title</property> property, or <jk>null</jk> if it is not set.
	 */
	public String getTitle() {
		return title;
	}

	/**
	 * Bean property setter:  <property>title</property>.
	 *
	 * @param title The new value for the <property>title</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setTitle(String title) {
		this.title = title;
		return this;
	}

	/**
	 * Bean property getter:  <property>description</property>.
	 *
	 * @return The value of the <property>description</property> property, or <jk>null</jk> if it is not set.
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * Bean property setter:  <property>description</property>.
	 *
	 * @param description The new value for the <property>description</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setDescription(String description) {
		this.description = description;
		return this;
	}

	/**
	 * Bean property getter:  <property>type</property>.
	 *
	 * @return
	 * 	The value of the <property>type</property> property on this bean, or <jk>null</jk> if it is not set.
	 * 	Can be either a {@link JsonType} or {@link JsonTypeArray} depending on what value was used to set it.
	 */
	@Swap(JsonTypeOrJsonTypeArraySwap.class)
	public Object getType() {
		if (typeJsonType != null)
			return typeJsonType;
		return typeJsonTypeArray;
	}

	/**
	 * Bean property getter:  <property>type</property>.
	 *
	 * <p>
	 * Convenience method for returning the <property>type</property> property when it is a {@link JsonType} value.
	 *
	 * @return
	 * 	The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link JsonTypeArray}.
	 */
	public JsonType getTypeAsJsonType() {
		return typeJsonType;
	}

	/**
	 * Bean property getter:  <property>type</property>.
	 *
	 * <p>
	 * Convenience method for returning the <property>type</property> property when it is a {@link JsonTypeArray} value.
	 *
	 * @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link JsonType}.
	 */
	public JsonTypeArray getTypeAsJsonTypeArray() {
		return typeJsonTypeArray;
	}

	/**
	 * Bean property setter:  <property>type</property>.
	 *
	 * @param type
	 * 	The new value for the <property>type</property> property on this bean.
	 * 	This object must be of type {@link JsonType} or {@link JsonTypeArray}.
	 * @return This object.
	 * @throws BeanRuntimeException If invalid object type passed in.
	 */
	public JsonSchema setType(Object type) {
		this.typeJsonType = null;
		this.typeJsonTypeArray = null;
		if (type != null) {
			if (type instanceof JsonType)
				this.typeJsonType = (JsonType)type;
			else if (type instanceof JsonTypeArray)
				this.typeJsonTypeArray = (JsonTypeArray)type;
			else
				throw new BeanRuntimeException(JsonSchemaProperty.class, "Invalid attribute type ''{0}'' passed in.  Must be one of the following:  SimpleType, SimpleTypeArray", className(type));
		}
		return this;
	}

	/**
	 * Bean property appender:  <property>type</property>.
	 *
	 * @param types The list of items to append to the <property>type</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addTypes(JsonType...types) {
		if (this.typeJsonTypeArray == null)
			this.typeJsonTypeArray = new JsonTypeArray();
		this.typeJsonTypeArray.addAll(types);
		return this;
	}

	/**
	 * Used during parsing to convert the <property>type</property> property to the correct class type.
	 *
	 * <ul class='spaced-list'>
	 * 	<li>
	 * 		If parsing a JSON-array, converts to a {@link JsonTypeArray}.
	 * 	<li>
	 * 		If parsing a JSON-object, converts to a {@link JsonType}.
	 * </ul>
	 *
	 * <p>
	 * Serialization method is a no-op.
	 */
	public static class JsonTypeOrJsonTypeArraySwap extends ObjectSwap<Object,Object> {

		@Override /* ObjectSwap */
		public Object swap(BeanSession session, Object o) throws SerializeException {
			return o;
		}

		@Override /* ObjectSwap */
		public Object unswap(BeanSession session, Object o, ClassMeta<?> hint) throws ParseException {
			ClassMeta<?> cm = (
				o instanceof Collection
				? session.getClassMeta(JsonTypeArray.class)
				: session.getClassMeta(JsonType.class)
			);
			return session.convertToType(o, cm);
		}
	}

	/**
	 * Bean property getter:  <property>definitions</property>.
	 *
	 * @return
	 * 	The value of the <property>definitions</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Map<String,JsonSchema> getDefinitions() {
		return definitions;
	}

	/**
	 * Bean property setter:  <property>definitions</property>.
	 *
	 * @param definitions The new value for the <property>definitions</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setDefinitions(Map<String,JsonSchema> definitions) {
		this.definitions = definitions;
		if (definitions != null)
			setMasterOn(definitions.values());
		return this;
	}

	/**
	 * Bean property appender:  <property>definitions</property>.
	 *
	 * @param name The key in the definitions map entry.
	 * @param definition The value in the definitions map entry.
	 * @return This object.
	 */
	public JsonSchema addDefinition(String name, JsonSchema definition) {
		if (this.definitions == null)
			this.definitions = map();
		this.definitions.put(name, definition);
		setMasterOn(definition);
		return this;
	}

	/**
	 * Bean property getter:  <property>properties</property>.
	 *
	 * @return The value of the <property>properties</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Map<String,JsonSchema> getProperties() {
		return properties;
	}

	/**
	 * Returns the property with the specified name.
	 *
	 * <p>
	 * This is equivalent to calling <property>getProperty(name, <jk>false</jk>)</property>.
	 *
	 * @param name The property name.
	 * @return The property with the specified name, or <jk>null</jk> if no property is specified.
	 */
	public JsonSchema getProperty(String name) {
		return getProperty(name, false);
	}

	/**
	 * Returns the property with the specified name.
	 *
	 * <p>
	 * If <property>resolve</property> is <jk>true</jk>, the property object will automatically be  resolved by calling
	 * {@link #resolve()}.
	 * Therefore, <property>getProperty(name, <jk>true</jk>)</property> is equivalent to calling
	 * <property>getProperty(name).resolve()</property>, except it's safe from a potential
	 * <property>NullPointerException</property>.
	 *
	 * @param name The property name.
	 * @param resolve If <jk>true</jk>, calls {@link #resolve()} on object before returning.
	 * @return The property with the specified name, or <jk>null</jk> if no property is specified.
	 */
	public JsonSchema getProperty(String name, boolean resolve) {
		if (properties == null)
			return null;
		JsonSchema s = properties.get(name);
		if (s == null)
			return null;
		if (resolve)
			s = s.resolve();
		return s;
	}

	/**
	 * Bean property setter:  <property>properties</property>.
	 *
	 * @param properties The new value for the <property>properties</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setProperties(Map<String,JsonSchema> properties) {
		this.properties = properties;
		if (properties != null) {
			properties.entrySet().forEach(x -> {
				JsonSchema value = x.getValue();
				setMasterOn(value);
				value.setName(x.getKey());
			});
		}
		return this;
	}

	/**
	 * Bean property appender:  <property>properties</property>.
	 *
	 * <p>
	 * Properties must have their <property>name</property> property set on them when using this method.
	 *
	 * @param properties The list of items to append to the <property>properties</property> property on this bean.
	 * @return This object.
	 * @throws BeanRuntimeException If property is found without a set <property>name</property> property.
	 */
	public JsonSchema addProperties(JsonSchema...properties) {
		if (this.properties == null)
			this.properties = map();
		for (JsonSchema p : properties) {
			if (p.getName() == null)
				throw new BeanRuntimeException(JsonSchema.class,
					"Invalid property passed to JsonSchema.addProperties().  Property name was null.");
			setMasterOn(p);
			this.properties.put(p.getName(), p);
		}
		return this;
	}

	/**
	 * Bean property getter:  <property>patternProperties</property>.
	 *
	 * @return
	 * 	The value of the <property>patternProperties</property> property on this bean, or <jk>null</jk> if it is
	 * 	not set.
	 */
	public Map<String,JsonSchema> getPatternProperties() {
		return patternProperties;
	}

	/**
	 * Bean property setter:  <property>patternProperties</property>.
	 *
	 * @param patternProperties The new value for the <property>patternProperties</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setPatternProperties(Map<String,JsonSchema> patternProperties) {
		this.patternProperties = patternProperties;
		if (patternProperties != null) {
			patternProperties.entrySet().forEach(x -> {
				JsonSchema s = x.getValue();
				setMasterOn(s);
				s.setName(x.getKey());
			});
		}
		return this;
	}

	/**
	 * Bean property appender:  <property>patternProperties</property>.
	 *
	 * <p>
	 * Properties must have their <property>name</property> property set to the pattern string when using this method.
	 *
	 * @param properties The list of items to append to the <property>patternProperties</property> property on this bean.
	 * @return This object.
	 * @throws BeanRuntimeException If property is found without a set <property>name</property> property.
	 */
	public JsonSchema addPatternProperties(JsonSchemaProperty...properties) {
		if (this.patternProperties == null)
			this.patternProperties = map();
		for (JsonSchema p : properties) {
			if (p.getName() == null)
				throw new BeanRuntimeException(JsonSchema.class,
					"Invalid property passed to JsonSchema.addProperties().  Property name was null.");
			setMasterOn(p);
			this.patternProperties.put(p.getName(), p);
		}
		return this;
	}

	/**
	 * Bean property getter:  <property>dependencies</property>.
	 *
	 * @return
	 * 	The value of the <property>dependencies</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Map<String,JsonSchema> getDependencies() {
		return dependencies;
	}

	/**
	 * Bean property setter:  <property>dependencies</property>.
	 *
	 * @param dependencies The new value for the <property>dependencies</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setDependencies(Map<String,JsonSchema> dependencies) {
		this.dependencies = dependencies;
		if (dependencies != null)
			setMasterOn(dependencies.values());
		return this;
	}

	/**
	 * Bean property appender:  <property>dependencies</property>.
	 *
	 * @param name The key of the entry in the dependencies map.
	 * @param dependency The value of the entry in the dependencies map.
	 * @return This object.
	 */
	public JsonSchema addDependency(String name, JsonSchema dependency) {
		if (this.dependencies == null)
			this.dependencies = map();
		this.dependencies.put(name, dependency);
		setMasterOn(dependency);
		return this;
	}

	/**
	 * Bean property getter:  <property>items</property>.
	 *
	 * @return
	 * 	The value of the <property>items</property> property on this bean, or <jk>null</jk> if it is not set.
	 * 	Can be either a {@link JsonSchema} or {@link JsonSchemaArray} depending on what value was used to set it.
	 */
	@Swap(JsonSchemaOrSchemaArraySwap.class)
	public Object getItems() {
		if (itemsSchema != null)
			return itemsSchema;
		return itemsSchemaArray;
	}

	/**
	 * Bean property getter:  <property>items</property>.
	 *
	 * <p>
	 * Convenience method for returning the <property>items</property> property when it is a {@link JsonSchema} value.
	 *
	 * @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link JsonSchemaArray}.
	 */
	public JsonSchema getItemsAsSchema() {
		return itemsSchema;
	}

	/**
	 * Bean property getter:  <property>items</property>.
	 *
	 * <p>
	 * Convenience method for returning the <property>items</property> property when it is a {@link JsonSchemaArray} value.
	 *
	 * @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link JsonSchema}.
	 */
	public JsonSchemaArray getItemsAsSchemaArray() {
		return itemsSchemaArray;
	}

	/**
	 * Used during parsing to convert the <property>items</property> property to the correct class type.
	 *
	 * <ul class='spaced-list'>
	 * 	<li>
	 * 		If parsing a JSON-array, converts to a {@link JsonSchemaArray}.
	 * 	<li>
	 * 		If parsing a JSON-object, converts to a {@link JsonSchema}.
	 * </ul>
	 *
	 * <p>
	 * Serialization method is a no-op.
	 */
	public static class JsonSchemaOrSchemaArraySwap extends ObjectSwap<Object,Object> {

		@Override /* ObjectSwap */
		public Object swap(BeanSession session, Object o) throws SerializeException {
			return o;
		}

		@Override /* ObjectSwap */
		public Object unswap(BeanSession session, Object o, ClassMeta<?> hint) throws ParseException {
			ClassMeta<?> cm = (
				o instanceof Collection
				? session.getClassMeta(JsonSchemaArray.class)
				: session.getClassMeta(JsonSchema.class)
			);
			return session.convertToType(o, cm);
		}
	}

	/**
	 * Bean property setter:  <property>items</property>.
	 *
	 * @param
	 * 	items The new value for the <property>items</property> property on this bean.
	 * 	This object must be of type {@link JsonSchema} or {@link JsonSchemaArray}.
	 * @return This object.
	 * @throws BeanRuntimeException If invalid object type passed in.
	 */
	public JsonSchema setItems(Object items) {
		this.itemsSchema = null;
		this.itemsSchemaArray = null;
		if (items != null) {
			if (items instanceof JsonSchema) {
				this.itemsSchema = (JsonSchema)items;
				setMasterOn(this.itemsSchema);
			} else if (items instanceof JsonSchemaArray) {
				this.itemsSchemaArray = (JsonSchemaArray)items;
				setMasterOn(this.itemsSchemaArray);
			} else {
				throw new BeanRuntimeException(JsonSchemaProperty.class, "Invalid attribute type ''{0}'' passed in.  Must be one of the following:  JsonSchema, JsonSchemaArray", className(items));
			}
		}
		return this;
	}

	/**
	 * Bean property appender:  <property>items</property>.
	 *
	 * @param items The list of items to append to the <property>items</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addItems(JsonSchema...items) {
		if (this.itemsSchemaArray == null)
			this.itemsSchemaArray = new JsonSchemaArray();
		this.itemsSchemaArray.addAll(items);
		setMasterOn(items);
		return this;
	}

	/**
	 * Bean property getter:  <property>multipleOf</property>.
	 *
	 * @return The value of the <property>multipleOf</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Number getMultipleOf() {
		return multipleOf;
	}

	/**
	 * Bean property setter:  <property>multipleOf</property>.
	 *
	 * @param multipleOf The new value for the <property>multipleOf</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMultipleOf(Number multipleOf) {
		this.multipleOf = multipleOf;
		return this;
	}

	/**
	 * Bean property getter:  <property>maximum</property>.
	 *
	 * @return The value of the <property>maximum</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Number getMaximum() {
		return maximum;
	}

	/**
	 * Bean property setter:  <property>maximum</property>.
	 *
	 * @param maximum The new value for the <property>maximum</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMaximum(Number maximum) {
		this.maximum = maximum;
		return this;
	}

	/**
	 * Bean property getter:  <property>exclusiveMaximum</property>.
	 *
	 * @return
	 * 	The value of the <property>exclusiveMaximum</property> property on this bean, or <jk>null</jk> if it is
	 * 	not set.
	 */
	public Boolean isExclusiveMaximum() {
		return exclusiveMaximum;
	}

	/**
	 * Bean property setter:  <property>exclusiveMaximum</property>.
	 *
	 * @param exclusiveMaximum The new value for the <property>exclusiveMaximum</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setExclusiveMaximum(Boolean exclusiveMaximum) {
		this.exclusiveMaximum = exclusiveMaximum;
		return this;
	}

	/**
	 * Bean property getter:  <property>minimum</property>.
	 *
	 * @return The value of the <property>minimum</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Number getMinimum() {
		return minimum;
	}

	/**
	 * Bean property setter:  <property>minimum</property>.
	 *
	 * @param minimum The new value for the <property>minimum</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMinimum(Number minimum) {
		this.minimum = minimum;
		return this;
	}

	/**
	 * Bean property getter:  <property>exclusiveMinimum</property>.
	 *
	 * @return
	 * 	The value of the <property>exclusiveMinimum</property> property on this bean, or <jk>null</jk> if it is
	 * 	not set.
	 */
	public Boolean isExclusiveMinimum() {
		return exclusiveMinimum;
	}

	/**
	 * Bean property setter:  <property>exclusiveMinimum</property>.
	 *
	 * @param exclusiveMinimum The new value for the <property>exclusiveMinimum</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setExclusiveMinimum(Boolean exclusiveMinimum) {
		this.exclusiveMinimum = exclusiveMinimum;
		return this;
	}

	/**
	 * Bean property getter:  <property>maxLength</property>.
	 *
	 * @return The value of the <property>maxLength</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Integer getMaxLength() {
		return maxLength;
	}

	/**
	 * Bean property setter:  <property>maxLength</property>.
	 *
	 * @param maxLength The new value for the <property>maxLength</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMaxLength(Integer maxLength) {
		this.maxLength = maxLength;
		return this;
	}

	/**
	 * Bean property getter:  <property>minLength</property>.
	 *
	 * @return The value of the <property>minLength</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Integer getMinLength() {
		return minLength;
	}

	/**
	 * Bean property setter:  <property>minLength</property>.
	 *
	 * @param minLength The new value for the <property>minLength</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMinLength(Integer minLength) {
		this.minLength = minLength;
		return this;
	}

	/**
	 * Bean property getter:  <property>pattern</property>.
	 *
	 * @return The value of the <property>pattern</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public String getPattern() {
		return pattern;
	}

	/**
	 * Bean property setter:  <property>pattern</property>.
	 *
	 * @param pattern The new value for the <property>pattern</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setPattern(String pattern) {
		this.pattern = pattern;
		return this;
	}

	/**
	 * Bean property getter:  <property>additionalItems</property>.
	 *
	 * @return
	 * 	The value of the <property>additionalItems</property> property on this bean, or <jk>null</jk> if it is
	 * 	not set.
	 * 	Can be either a {@link Boolean} or {@link JsonSchemaArray} depending on what value was used to set it.
	 */
	@Swap(BooleanOrSchemaArraySwap.class)
	public Object getAdditionalItems() {
		if (additionalItemsBoolean != null)
			return additionalItemsBoolean;
		return additionalItemsSchemaArray;
	}

	/**
	 * Bean property getter:  <property>additionalItems</property>.
	 *
	 * <p>
	 * Convenience method for returning the <property>additionalItems</property> property when it is a {@link Boolean}
	 * value.
	 *
	 * @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link JsonSchemaArray}.
	 */
	public Boolean getAdditionalItemsAsBoolean() {
		return additionalItemsBoolean;
	}

	/**
	 * Bean property getter:  <property>additionalItems</property>.
	 *
	 * <p>
	 * Convenience method for returning the <property>additionalItems</property> property when it is a
	 * {@link JsonSchemaArray} value.
	 *
	 * @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link Boolean}.
	 */
	public List<JsonSchema> getAdditionalItemsAsSchemaArray() {
		return additionalItemsSchemaArray;
	}

	/**
	 * Bean property setter:  <property>additionalItems</property>.
	 *
	 * @param additionalItems
	 * 	The new value for the <property>additionalItems</property> property on this bean.
	 * 	This object must be of type {@link Boolean} or {@link JsonSchemaArray}.
	 * @return This object.
	 * @throws BeanRuntimeException If invalid object type passed in.
	 */
	public JsonSchema setAdditionalItems(Object additionalItems) {
		this.additionalItemsBoolean = null;
		this.additionalItemsSchemaArray = null;
		if (additionalItems != null) {
			if (additionalItems instanceof Boolean)
				this.additionalItemsBoolean = (Boolean)additionalItems;
			else if (additionalItems instanceof JsonSchemaArray) {
				this.additionalItemsSchemaArray = (JsonSchemaArray)additionalItems;
				setMasterOn(this.additionalItemsSchemaArray);
			} else {
				throw new BeanRuntimeException(JsonSchemaProperty.class, "Invalid attribute type ''{0}'' passed in.  Must be one of the following:  Boolean, JsonSchemaArray", className(additionalItems));
			}
		}
		return this;
	}

	/**
	 * Bean property appender:  <property>additionalItems</property>.
	 *
	 * @param additionalItems
	 * 	The list of items to append to the <property>additionalItems</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addAdditionalItems(JsonSchema...additionalItems) {
		if (this.additionalItemsSchemaArray == null)
			this.additionalItemsSchemaArray = new JsonSchemaArray();
		this.additionalItemsSchemaArray.addAll(additionalItems);
		setMasterOn(additionalItems);
		return this;
	}

	/**
	 * Used during parsing to convert the <property>additionalItems</property> property to the correct class type.
	 *
	 * <ul class='spaced-list'>
	 * 	<li>
	 * 		If parsing a JSON-array, converts to a {@link JsonSchemaArray}.
	 * 	<li>
	 * 		If parsing a JSON-boolean, converts to a {@link Boolean}.
	 * </ul>
	 *
	 * <p>
	 * Serialization method is a no-op.
	 */
	public static class BooleanOrSchemaArraySwap extends ObjectSwap<Object,Object> {

		@Override /* ObjectSwap */
		public Object swap(BeanSession session, Object o) throws SerializeException {
			return o;
		}

		@Override /* ObjectSwap */
		public Object unswap(BeanSession session, Object o, ClassMeta<?> hint) throws ParseException {
			ClassMeta<?> cm = (
				o instanceof Collection
				? session.getClassMeta(JsonSchemaArray.class)
				: session.getClassMeta(Boolean.class)
			);
			return session.convertToType(o, cm);
		}
	}

	/**
	 * Bean property getter:  <property>maxItems</property>.
	 *
	 * @return The value of the <property>maxItems</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Integer getMaxItems() {
		return maxItems;
	}

	/**
	 * Bean property setter:  <property>maxItems</property>.
	 *
	 * @param maxItems The new value for the <property>maxItems</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMaxItems(Integer maxItems) {
		this.maxItems = maxItems;
		return this;
	}

	/**
	 * Bean property getter:  <property>minItems</property>.
	 *
	 * @return The value of the <property>minItems</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Integer getMinItems() {
		return minItems;
	}

	/**
	 * Bean property setter:  <property>minItems</property>.
	 *
	 * @param minItems The new value for the <property>minItems</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMinItems(Integer minItems) {
		this.minItems = minItems;
		return this;
	}

	/**
	 * Bean property getter:  <property>uniqueItems</property>.
	 *
	 * @return
	 * 	The value of the <property>uniqueItems</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Boolean getUniqueItems() {
		return uniqueItems;
	}

	/**
	 * Bean property setter:  <property>uniqueItems</property>.
	 *
	 * @param uniqueItems The new value for the <property>uniqueItems</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setUniqueItems(Boolean uniqueItems) {
		this.uniqueItems = uniqueItems;
		return this;
	}

	/**
	 * Bean property getter:  <property>maxProperties</property>.
	 *
	 * @return
	 * 	The value of the <property>maxProperties</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Integer getMaxProperties() {
		return maxProperties;
	}

	/**
	 * Bean property setter:  <property>maxProperties</property>.
	 *
	 * @param maxProperties The new value for the <property>maxProperties</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMaxProperties(Integer maxProperties) {
		this.maxProperties = maxProperties;
		return this;
	}

	/**
	 * Bean property getter:  <property>minProperties</property>.
	 *
	 * @return
	 * 	The value of the <property>minProperties</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public Integer getMinProperties() {
		return minProperties;
	}

	/**
	 * Bean property setter:  <property>minProperties</property>.
	 *
	 * @param minProperties The new value for the <property>minProperties</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setMinProperties(Integer minProperties) {
		this.minProperties = minProperties;
		return this;
	}

	/**
	 * Bean property getter:  <property>required</property>.
	 *
	 * @return The value of the <property>required</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public List<String> getRequired() {
		return required;
	}

	/**
	 * Bean property setter:  <property>required</property>.
	 *
	 * @param required The new value for the <property>required</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setRequired(List<String> required) {
		this.required = required;
		return this;
	}

	/**
	 * Bean property appender:  <property>required</property>.
	 *
	 * @param required The list of items to append to the <property>required</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addRequired(List<String> required) {
		if (this.required == null)
			this.required = new LinkedList<>();
		required.forEach(x -> this.required.add(x));
		return this;
	}

	/**
	 * Bean property appender:  <property>required</property>.
	 *
	 * @param required The list of items to append to the <property>required</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addRequired(String...required) {
		if (this.required == null)
			this.required = new LinkedList<>();
		for (String r : required)
			this.required.add(r);
		return this;
	}

	/**
	 * Bean property appender:  <property>required</property>.
	 *
	 * @param properties The list of items to append to the <property>required</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addRequired(JsonSchemaProperty...properties) {
		if (this.required == null)
			this.required = new LinkedList<>();
		for (JsonSchemaProperty p : properties)
			this.required.add(p.getName());
		return this;
	}

	/**
	 * Bean property getter:  <property>additionalProperties</property>.
	 *
	 * @return
	 * 	The value of the <property>additionalProperties</property> property on this bean, or <jk>null</jk> if it
	 * 	is not set.
	 * 	Can be either a {@link Boolean} or {@link JsonSchemaArray} depending on what value was used to set it.
	 */
	@Swap(BooleanOrSchemaSwap.class)
	public Object getAdditionalProperties() {
		if (additionalPropertiesBoolean != null)
			return additionalItemsBoolean;
		return additionalPropertiesSchema;
	}

	/**
	 * Bean property getter:  <property>additionalProperties</property>.
	 *
	 * <p>
	 * Convenience method for returning the <property>additionalProperties</property> property when it is a
	 * {@link Boolean} value.
	 *
	 * @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link JsonSchema}.
	 */
	public Boolean getAdditionalPropertiesAsBoolean() {
		return additionalPropertiesBoolean;
	}

	/**
	 * Bean property getter:  <property>additionalProperties</property>.
	 *
	 * <p>
	 * Convenience method for returning the <property>additionalProperties</property> property when it is a
	 * {@link JsonSchema} value.
	 *
	 * @return The currently set value, or <jk>null</jk> if the property is not set, or is set as a {@link Boolean}.
	 */
	public JsonSchema getAdditionalPropertiesAsSchema() {
		return additionalPropertiesSchema;
	}

	/**
	 * Bean property setter:  <property>additionalProperties</property>.
	 *
	 * @param additionalProperties
	 * 	The new value for the <property>additionalProperties</property> property on this bean.
	 * 	This object must be of type {@link Boolean} or {@link JsonSchema}.
	 * @return This object.
	 * @throws BeanRuntimeException If invalid object type passed in.
	 */
	@Beanp(dictionary={JsonSchema.class})
	public JsonSchema setAdditionalProperties(Object additionalProperties) {
		this.additionalPropertiesBoolean = null;
		this.additionalPropertiesSchema = null;
		if (additionalProperties != null) {
			if (additionalProperties instanceof Boolean)
				this.additionalPropertiesBoolean = (Boolean)additionalProperties;
			else if (additionalProperties instanceof JsonSchema) {
				this.additionalPropertiesSchema = (JsonSchema)additionalProperties;
				setMasterOn(this.additionalPropertiesSchema);
			} else
				throw new BeanRuntimeException(JsonSchemaProperty.class,
					"Invalid attribute type ''{0}'' passed in.  Must be one of the following:  Boolean, JsonSchema",
					className(additionalProperties));
		}
		return this;
	}

	/**
	 * Used during parsing to convert the <property>additionalProperties</property> property to the correct class type.
	 *
	 * <ul class='spaced-list'>
	 * 	<li>
	 * 		If parsing a JSON-object, converts to a {@link JsonSchema}.
	 * 	<li>
	 * 		If parsing a JSON-boolean, converts to a {@link Boolean}.
	 * </ul>
	 *
	 * <p>
	 * Serialization method is a no-op.
	 */
	public static class BooleanOrSchemaSwap extends ObjectSwap<Object,Object> {

		@Override /* ObjectSwap */
		public Object swap(BeanSession session, Object o) throws SerializeException {
			return o;
		}

		@Override /* ObjectSwap */
		public Object unswap(BeanSession session, Object o, ClassMeta<?> hint) throws ParseException {
			ClassMeta<?> cm = (
				o instanceof Boolean
				? session.getClassMeta(Boolean.class)
				: session.getClassMeta(JsonSchema.class)
			);
			return session.convertToType(o, cm);
		}
	}

	/**
	 * Bean property getter:  <property>enum</property>.
	 *
	 * @return The value of the <property>enum</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public List<String> getEnum() {
		return _enum;
	}

	/**
	 * Bean property setter:  <property>enum</property>.
	 *
	 * @param _enum The new value for the <property>enum</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setEnum(List<String> _enum) {
		this._enum = _enum;
		return this;
	}

	/**
	 * Bean property appender:  <property>enum</property>.
	 *
	 * @param _enum The list of items to append to the <property>enum</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addEnum(String..._enum) {
		if (this._enum == null)
			this._enum = new LinkedList<>();
		for (String e : _enum)
			this._enum.add(e);
		return this;
	}

	/**
	 * Bean property getter:  <property>allOf</property>.
	 *
	 * @return The value of the <property>allOf</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public List<JsonSchema> getAllOf() {
		return allOf;
	}

	/**
	 * Bean property setter:  <property>allOf</property>.
	 *
	 * @param allOf The new value for the <property>allOf</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setAllOf(List<JsonSchema> allOf) {
		this.allOf = allOf;
		setMasterOn(allOf);
		return this;
	}

	/**
	 * Bean property appender:  <property>allOf</property>.
	 *
	 * @param allOf The list of items to append to the <property>allOf</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addAllOf(JsonSchema...allOf) {
		setMasterOn(allOf);
		this.allOf = addAll(this.allOf, allOf);
		return this;
	}

	/**
	 * Bean property getter:  <property>anyOf</property>.
	 *
	 * @return The value of the <property>anyOf</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public List<JsonSchema> getAnyOf() {
		return anyOf;
	}

	/**
	 * Bean property setter:  <property>anyOf</property>.
	 *
	 * @param anyOf The new value of the <property>anyOf</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setAnyOf(List<JsonSchema> anyOf) {
		this.anyOf = anyOf;
		setMasterOn(anyOf);
		return this;
	}

	/**
	 * Bean property appender:  <property>anyOf</property>.
	 *
	 * @param anyOf The list of items to append to the <property>anyOf</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addAnyOf(JsonSchema...anyOf) {
		if (this.anyOf == null)
			this.anyOf = new LinkedList<>();
		setMasterOn(anyOf);
		for (JsonSchema s : anyOf)
			this.anyOf.add(s);
		return this;
	}

	/**
	 * Bean property getter:  <property>oneOf</property>.
	 *
	 * @return The value of the <property>oneOf</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public List<JsonSchema> getOneOf() {
		return oneOf;
	}

	/**
	 * Bean property setter:  <property>oneOf</property>.
	 *
	 * @param oneOf The new value for the <property>oneOf</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setOneOf(List<JsonSchema> oneOf) {
		this.oneOf = oneOf;
		setMasterOn(oneOf);
		return this;
	}

	/**
	 * Bean property appender:  <property>oneOf</property>.
	 *
	 * @param oneOf The list of items to append to the <property>oneOf</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema addOneOf(JsonSchema...oneOf) {
		if (this.oneOf == null)
			this.oneOf = new LinkedList<>();
		setMasterOn(oneOf);
		for (JsonSchema s : oneOf)
			this.oneOf.add(s);
		return this;
	}

	/**
	 * Bean property getter:  <property>not</property>.
	 *
	 * @return The value of the <property>not</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	public JsonSchema getNot() {
		return not;
	}

	/**
	 * Bean property setter:  <property>not</property>.
	 *
	 * @param not The new value for the <property>not</property> property on this bean.
	 * @return This object.
	 */
	public JsonSchema setNot(JsonSchema not) {
		this.not = not;
		setMasterOn(not);
		return this;
	}

	/**
	 * Bean property getter:  <property>$ref</property>.
	 *
	 * @return The value of the <property>$ref</property> property on this bean, or <jk>null</jk> if it is not set.
	 */
	@Beanp("$ref")
	public URI getRef() {
		return ref;
	}

	/**
	 * Bean property setter:  <property>$ref</property>.
	 *
	 * <p>
	 * The value can be of any of the following types: {@link URI}, {@link URL}, {@link String}.
	 * Strings must be valid URIs.
	 *
	 * <p>
	 * URIs defined by {@link UriResolver} can be used for values.
	 *
	 * @param ref The new value for the <property>$ref</property> property on this bean.
	 * @return This object.
	 */
	@Beanp("$ref")
	public JsonSchema setRef(Object ref) {
		this.ref = toURI(ref);
		return this;
	}

	private void setMasterOn(JsonSchema s) {
		if (s != null)
			s.setMaster(this);
	}

	private void setMasterOn(JsonSchema[] ss) {
		if (ss != null)
			for (JsonSchema s : ss)
				setMasterOn(s);
	}

	private void setMasterOn(Collection<JsonSchema> ss) {
		if (ss != null)
			ss.forEach(this::setMasterOn);
	}

	private void setMasterOn(JsonSchemaArray ss) {
		if (ss != null)
			ss.forEach(this::setMasterOn);
	}

	/**
	 * Sets the master schema for this schema and all child schema objects.
	 *
	 * <p>
	 * All child elements in a schema should point to a single "master" schema in order to locate registered JsonSchemaMap
	 * objects for resolving external schemas.
	 *
	 * @param master The master schema to associate on this and all children.  Can be <jk>null</jk>.
	 */
	protected void setMaster(JsonSchema master) {
		this.master = master;
		if (definitions != null)
			definitions.values().forEach(x -> x.setMaster(master));
		if (properties != null)
			properties.values().forEach(x -> x.setMaster(master));
		if (patternProperties != null)
			patternProperties.values().forEach(x -> x.setMaster(master));
		if (dependencies != null)
			dependencies.values().forEach(x -> x.setMaster(master));
		if (itemsSchema != null)
			itemsSchema.setMaster(master);
		if (itemsSchemaArray != null)
			itemsSchemaArray.forEach(x -> x.setMaster(master));
		if (additionalItemsSchemaArray != null)
			additionalItemsSchemaArray.forEach(x -> x.setMaster(master));
		if (additionalPropertiesSchema != null)
			additionalPropertiesSchema.setMaster(master);
		if (allOf != null)
			allOf.forEach(x -> x.setMaster(master));
		if (anyOf != null)
			anyOf.forEach(x -> x.setMaster(master));
		if (oneOf != null)
			oneOf.forEach(x -> x.setMaster(master));
		if (not != null)
			not.setMaster(master);
	}

	/**
	 * Resolve schema if reference.
	 *
	 * <p>
	 * If this schema is a reference to another schema (has its <property>$ref</property> property set), this
	 * method will retrieve the referenced schema from the schema map registered with this schema.
	 *
	 * <p>
	 * If this schema is not a reference, or no schema map is registered with this schema, this method is a no-op and
	 * simply returns this object.
	 *
	 * @return The referenced schema, or <jk>null</jk>.
	 */
	public JsonSchema resolve() {
		if (ref == null || master.schemaMap == null)
			return this;
		return master.schemaMap.get(ref);
	}

	/**
	 * Associates a schema map with this schema for resolving other schemas identified through <property>$ref</property>
	 * properties.
	 *
	 * @param schemaMap The schema map to associate with this schema.  Can be <jk>null</jk>.
	 * @return This object.
	 */
	public JsonSchema setSchemaMap(JsonSchemaMap schemaMap) {
		this.schemaMap = schemaMap;
		return this;
	}

	@Override /* Object */
	public String toString() {
		return JsonSerializer.DEFAULT.toString(this);
	}
}
