org.wrml.runtime.schema.generator.SchemaGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.wrml.runtime.schema.generator.SchemaGenerator.java

Source

/**
 * WRML - Web Resource Modeling Language
 *  __     __   ______   __    __   __
 * /\ \  _ \ \ /\  == \ /\ "-./  \ /\ \
 * \ \ \/ ".\ \\ \  __< \ \ \-./\ \\ \ \____
 *  \ \__/".~\_\\ \_\ \_\\ \_\ \ \_\\ \_____\
 *   \/_/   \/_/ \/_/ /_/ \/_/  \/_/ \/_____/
 *
 * http://www.wrml.org
 *
 * Copyright (C) 2011 - 2013 Mark Masse <mark@wrml.org> (OSS project WRML.org)
 *
 * Licensed 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.wrml.runtime.schema.generator;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.objectweb.asm.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wrml.model.MaybeReadOnly;
import org.wrml.model.MaybeRequired;
import org.wrml.model.Model;
import org.wrml.model.rest.LinkRelation;
import org.wrml.model.rest.Method;
import org.wrml.model.schema.*;
import org.wrml.runtime.Context;
import org.wrml.runtime.format.application.schema.json.JsonSchema;
import org.wrml.runtime.format.application.schema.json.JsonSchema.Definitions.JsonType;
import org.wrml.runtime.format.application.schema.json.JsonSchema.Definitions.PropertyType;
import org.wrml.runtime.format.application.schema.json.JsonSchema.JsonStringFormat;
import org.wrml.runtime.format.application.schema.json.JsonSchema.Property;
import org.wrml.runtime.format.application.schema.json.JsonSchemaLoader;
import org.wrml.runtime.rest.ApiLoader;
import org.wrml.runtime.schema.*;
import org.wrml.runtime.syntax.SyntaxHandler;
import org.wrml.runtime.syntax.SyntaxLoader;
import org.wrml.util.JavaBean;
import org.wrml.util.UniqueName;

import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>
 * Implements the transformation of Schema to Java Class (.class bytecode).
 * </p>
 */
public class SchemaGenerator implements Opcodes {

    private static final Logger LOG = LoggerFactory.getLogger(SchemaGenerator.class);

    private static final String ANNONYMOUS_INNER_SCHEMA_PREFIX = "$";

    private static final String ANNOTATION_INTERNAL_NAME_ALIASES = SchemaGenerator
            .classToInternalTypeName(Aliases.class);

    private static final String ANNOTATION_INTERNAL_NAME_COLLECTION_SLOT = SchemaGenerator
            .classToInternalTypeName(CollectionSlot.class);

    private static final String ANNOTATION_INTERNAL_NAME_COLLECTION_SLOT_CRITERION = SchemaGenerator
            .classToInternalTypeName(CollectionSlotCriterion.class);

    private static final String ANNOTATION_INTERNAL_NAME_DEFAULT_VALUE = SchemaGenerator
            .classToInternalTypeName(DefaultValue.class);

    private static final String ANNOTATION_INTERNAL_NAME_DESCRIPTION = SchemaGenerator
            .classToInternalTypeName(Description.class);

    private static final String ANNOTATION_INTERNAL_NAME_DISALLOWED_VALUES = SchemaGenerator
            .classToInternalTypeName(DisallowedValues.class);

    private static final String ANNOTATION_INTERNAL_NAME_DIVISIBLE_BY_VALUE = SchemaGenerator
            .classToInternalTypeName(DivisibleByValue.class);

    private static final String ANNOTATION_INTERNAL_NAME_LINK_SLOT = SchemaGenerator
            .classToInternalTypeName(LinkSlot.class);

    private static final String ANNOTATION_INTERNAL_NAME_LINK_SLOT_BINDING = SchemaGenerator
            .classToInternalTypeName(LinkSlotBinding.class);

    private static final String ANNOTATION_INTERNAL_NAME_MAXIMUM_LENGTH = SchemaGenerator
            .classToInternalTypeName(MaximumLength.class);

    private static final String ANNOTATION_INTERNAL_NAME_MAXIMUM_SIZE = SchemaGenerator
            .classToInternalTypeName(MaximumSize.class);

    private static final String ANNOTATION_INTERNAL_NAME_MAXIMUM_VALUE = SchemaGenerator
            .classToInternalTypeName(MaximumValue.class);

    private static final String ANNOTATION_INTERNAL_NAME_MINIMUM_LENGTH = SchemaGenerator
            .classToInternalTypeName(MinimumLength.class);

    private static final String ANNOTATION_INTERNAL_NAME_MINIMUM_SIZE = SchemaGenerator
            .classToInternalTypeName(MinimumSize.class);

    private static final String ANNOTATION_INTERNAL_NAME_MINIMUM_VALUE = SchemaGenerator
            .classToInternalTypeName(MinimumValue.class);

    private static final String ANNOTATION_INTERNAL_NAME_MULTILINE = SchemaGenerator
            .classToInternalTypeName(Multiline.class);

    private static final String ANNOTATION_INTERNAL_NAME_NON_NULL = SchemaGenerator
            .classToInternalTypeName(NonNull.class);

    private static final String ANNOTATION_INTERNAL_NAME_READ_ONLY = SchemaGenerator
            .classToInternalTypeName(ReadOnly.class);

    private static final String ANNOTATION_INTERNAL_NAME_TAGS = SchemaGenerator.classToInternalTypeName(Tags.class);

    private static final String ANNOTATION_INTERNAL_NAME_THUMBNAIL_IMAGE = SchemaGenerator
            .classToInternalTypeName(ThumbnailImage.class);

    private static final String ANNOTATION_INTERNAL_NAME_TITLE = SchemaGenerator
            .classToInternalTypeName(Title.class);

    private static final String ANNOTATION_INTERNAL_NAME_VERSION = SchemaGenerator
            .classToInternalTypeName(Version.class);

    private static final String ANNOTATION_INTERNAL_NAME_WRML = SchemaGenerator.classToInternalTypeName(WRML.class);

    private static final String CLOSE_PARENTHESIS = ")";

    private static final String EMPTY_PARENTHESES = SchemaGenerator.OPEN_PARENTHESIS
            + SchemaGenerator.CLOSE_PARENTHESIS;

    private static final String CLOSED_METHOD_DESCRIPTOR = "()V";

    private static final String INIT_METHOD_NAME = "<init>";

    private static final String CLINIT_METHOD_NAME = "<clinit>";

    private static final String VALUES_METHOD_NAME = "values";

    private static final String VALUE_OF_METHOD_NAME = "valueOf";

    private static final String $VALUES_CONSTANT_NAME = "$VALUES";

    private static final String CLONE_METHOD_NAME = "clone";

    /**
     * The access modifiers for Java interfaces.
     */
    private static final int INTERFACE_ACCESS = Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT + Opcodes.ACC_INTERFACE;

    /**
     * The access modifiers for Java interface methods.
     */
    private static final int INTERFACE_METHOD_ACCESS = Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT;

    private static final String OBJECT_INTERNAL_NAME = "java/lang/Object";

    private static final String CLASS_INTERNAL_NAME = "java/lang/Class";

    private static final String STRING_INTERNAL_NAME = "java/lang/String";

    private static final String ENUM_INTERNAL_NAME = "java/lang/Enum";

    private static final String INIT_METHOD_DESCRIPTOR = "(L" + STRING_INTERNAL_NAME + ";I)V";

    private static final String STRING_PARAM_METHOD_DESCRIPTOR_PREFIX = "(L" + STRING_INTERNAL_NAME + ";)";

    private static final String RETURN_OBJECT_METHOD_DESCRIPTOR = "()L" + OBJECT_INTERNAL_NAME + ";";

    private static final String VALUE_OF_METHOD_DESCRIPTOR = "(L" + CLASS_INTERNAL_NAME + ";L"
            + STRING_INTERNAL_NAME + ";)L" + ENUM_INTERNAL_NAME + ";";

    /**
     * JVM class file API version
     */
    private static final int JVM_VERSION = Opcodes.V1_5;

    private static final String MODEL_INTERFACE_INTERNAL_NAME = SchemaGenerator
            .classToInternalTypeName(Model.class);

    private static final String OPEN_PARENTHESIS = "(";

    private final JavaBean _ModelJavaBean;

    private final SchemaLoader _SchemaLoader;

    private final ConcurrentHashMap<URI, Integer> _InnerSchemaCountMap;

    /**
     * Create a new SchemaGenerator for the specified SchemaLoader.
     *
     * @param schemaLoader The SchemaLoader (a ClassLoader) instance that will use this SchemaGenerator to load Schema Java interfaces from Schema models and other sources.
     */
    public SchemaGenerator(final SchemaLoader schemaLoader) {

        _SchemaLoader = schemaLoader;
        if (_SchemaLoader == null) {
            throw new NullPointerException("The SchemaLoader is null");
        }

        _ModelJavaBean = new JavaBean(ValueType.JAVA_TYPE_MODEL, null);

        _InnerSchemaCountMap = new ConcurrentHashMap<URI, Integer>();

    }

    public static final String classToInternalTypeName(final Class<?> clazz) {

        return SchemaGenerator.externalTypeNameToInternalTypeName(clazz.getCanonicalName());
    }

    public static final String externalTypeNameToInternalTypeName(final String externalTypeName) {

        return externalTypeName.replace('.', '/');
    }

    public static final String internalTypeNameToExternalTypeName(final String internalTypeName) {

        return internalTypeName.replace('/', '.');
    }

    public Choices generateChoices(final Class<?> nativeChoicesEnumClass) {

        if (nativeChoicesEnumClass == null || !nativeChoicesEnumClass.isEnum()) {
            return null;
        }

        final SchemaLoader schemaLoader = getSchemaLoader();
        final Choices choices = getContext().newModel(schemaLoader.getChoicesSchemaUri());

        final URI choicesUri = schemaLoader.getTypeUri(nativeChoicesEnumClass);
        choices.setUri(choicesUri);

        final UniqueName choicesUniqueName = schemaLoader.getTypeUniqueName(choicesUri);
        choices.setUniqueName(choicesUniqueName);

        final Object[] enumConstants = nativeChoicesEnumClass.getEnumConstants();
        if (enumConstants != null && enumConstants.length > 0) {
            final LinkedHashSet choiceSet = new LinkedHashSet(enumConstants.length);

            for (final Object enumConstant : enumConstants) {
                final String choice = String.valueOf(enumConstant);
                choiceSet.add(enumConstant);
            }

            choices.getList().addAll(choiceSet);
        }

        return choices;
    }

    /**
     * Yay Bytecode!
     */
    public JavaBytecodeClass generateChoicesEnum(final Choices choices) {

        if (choices == null) {
            return null;
        }

        final List<String> choicesList = choices.getList();

        final URI choicesUri = choices.getUri();
        final String enumName = uriToInternalTypeName(choicesUri);
        final String enumTypeName = "L" + enumName + ";";
        final String enumArrayTypeName = "[" + enumTypeName;

        final JavaBytecodeClass enumByteCodeClass = new JavaBytecodeClass(enumName);
        final JavaBytecodeAnnotation wrmlAnnotation = new JavaBytecodeAnnotation(
                SchemaGenerator.ANNOTATION_INTERNAL_NAME_WRML);
        wrmlAnnotation.setAttributeValue(AnnotationParameterName.uniqueName.name(),
                choices.getUniqueName().getFullName());
        enumByteCodeClass.getAnnotations().add(wrmlAnnotation);

        final ClassWriter classWriter = new ClassWriter(0);

        FieldVisitor fieldVisitor;
        MethodVisitor methodVisitor;

        classWriter.visit(51, ACC_PUBLIC + ACC_FINAL + ACC_SUPER + ACC_ENUM, enumName,
                "L" + ENUM_INTERNAL_NAME + "<" + enumTypeName + ">;", ENUM_INTERNAL_NAME, null);

        {
            for (final String choice : choicesList) {
                fieldVisitor = classWriter.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC + ACC_ENUM, choice,
                        enumTypeName, null, null);
                fieldVisitor.visitEnd();
            }
        }

        {
            fieldVisitor = classWriter.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC + ACC_SYNTHETIC,
                    $VALUES_CONSTANT_NAME, enumArrayTypeName, null, null);
            fieldVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC + ACC_STATIC, VALUES_METHOD_NAME,
                    OPEN_PARENTHESIS + CLOSE_PARENTHESIS + enumArrayTypeName, null, null);
            methodVisitor.visitCode();
            methodVisitor.visitFieldInsn(GETSTATIC, enumName, $VALUES_CONSTANT_NAME, enumArrayTypeName);
            methodVisitor.visitMethodInsn(INVOKEVIRTUAL, enumArrayTypeName, CLONE_METHOD_NAME,
                    RETURN_OBJECT_METHOD_DESCRIPTOR);
            methodVisitor.visitTypeInsn(CHECKCAST, enumArrayTypeName);
            methodVisitor.visitInsn(ARETURN);
            methodVisitor.visitMaxs(1, 0);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_PUBLIC + ACC_STATIC, VALUE_OF_METHOD_NAME,
                    STRING_PARAM_METHOD_DESCRIPTOR_PREFIX + enumTypeName, null, null);
            methodVisitor.visitCode();
            methodVisitor.visitLdcInsn(org.objectweb.asm.Type.getType(enumTypeName));
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitMethodInsn(INVOKESTATIC, ENUM_INTERNAL_NAME, VALUE_OF_METHOD_NAME,
                    VALUE_OF_METHOD_DESCRIPTOR);
            methodVisitor.visitTypeInsn(CHECKCAST, enumName);
            methodVisitor.visitInsn(ARETURN);
            methodVisitor.visitMaxs(2, 1);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_PRIVATE, INIT_METHOD_NAME, INIT_METHOD_DESCRIPTOR,
                    CLOSED_METHOD_DESCRIPTOR, null);
            methodVisitor.visitCode();
            methodVisitor.visitVarInsn(ALOAD, 0);
            methodVisitor.visitVarInsn(ALOAD, 1);
            methodVisitor.visitVarInsn(ILOAD, 2);
            methodVisitor.visitMethodInsn(INVOKESPECIAL, ENUM_INTERNAL_NAME, INIT_METHOD_NAME,
                    INIT_METHOD_DESCRIPTOR);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(3, 3);
            methodVisitor.visitEnd();
        }
        {
            methodVisitor = classWriter.visitMethod(ACC_STATIC, CLINIT_METHOD_NAME, CLOSED_METHOD_DESCRIPTOR, null,
                    null);
            methodVisitor.visitCode();

            int constantIndex = 0;
            for (final String choice : choicesList) {
                methodVisitor.visitTypeInsn(NEW, enumName);
                methodVisitor.visitInsn(DUP);
                methodVisitor.visitLdcInsn(choice);

                visitConstantIncrement(methodVisitor, constantIndex);
                constantIndex++;

                methodVisitor.visitMethodInsn(INVOKESPECIAL, enumName, INIT_METHOD_NAME, INIT_METHOD_DESCRIPTOR);
                methodVisitor.visitFieldInsn(PUTSTATIC, enumName, choice, enumTypeName);
            }

            methodVisitor.visitIntInsn(BIPUSH, constantIndex);
            methodVisitor.visitTypeInsn(ANEWARRAY, enumName);

            constantIndex = 0;
            for (final String choice : choicesList) {
                methodVisitor.visitInsn(DUP);

                visitConstantIncrement(methodVisitor, constantIndex);
                constantIndex++;

                methodVisitor.visitFieldInsn(GETSTATIC, enumName, choice, enumTypeName);
                methodVisitor.visitInsn(AASTORE);
            }

            methodVisitor.visitFieldInsn(PUTSTATIC, enumName, $VALUES_CONSTANT_NAME, enumArrayTypeName);
            methodVisitor.visitInsn(RETURN);
            methodVisitor.visitMaxs(4, 0);
            methodVisitor.visitEnd();

        }
        classWriter.visitEnd();

        final byte[] bytecode = classWriter.toByteArray();

        enumByteCodeClass.setBytecode(bytecode);

        return enumByteCodeClass;
    }

    private void visitConstantIncrement(final MethodVisitor methodVisitor, final int constantIndex) {

        switch (constantIndex) {

        case 0:
            methodVisitor.visitInsn(ICONST_0);
            break;

        case 1:
            methodVisitor.visitInsn(ICONST_1);
            break;

        case 2:
            methodVisitor.visitInsn(ICONST_2);
            break;

        case 3:
            methodVisitor.visitInsn(ICONST_3);
            break;

        case 4:
            methodVisitor.visitInsn(ICONST_4);
            break;

        case 5:
            methodVisitor.visitInsn(ICONST_5);
            break;

        default:
            methodVisitor.visitIntInsn(BIPUSH, constantIndex);
            break;
        }

    }

    public Schema generateSchema(final JsonSchema jsonSchema, final URI... baseSchemaUris)
            throws SchemaGeneratorException {

        final URI schemaUri = jsonSchema.getId();
        final Context context = getContext();

        final Schema schema = context.newModel(getSchemaLoader().getSchemaDimensions());
        schema.setUri(schemaUri);

        final UniqueName literalUniqueName = JsonSchema.createJsonSchemaUniqueName(schemaUri);
        final String namespace = literalUniqueName.getNamespace();
        final String localName = StringUtils.capitalize(literalUniqueName.getLocalName());
        final UniqueName uniqueName = new UniqueName(namespace, localName);
        schema.setUniqueName(uniqueName);
        final String schemaDescription = jsonSchema.getDescription();
        if (schemaDescription != null) {
            schema.setDescription(schemaDescription);
        }

        final String schemaTitle = uniqueName.getLocalName();
        if (schemaTitle != null) {
            schema.setTitle(schemaTitle);
        }

        final Set<URI> baseSchemaUriSet = new HashSet<>();
        if (baseSchemaUris != null) {
            baseSchemaUriSet.addAll(Arrays.asList(baseSchemaUris));
        }

        baseSchemaUriSet.addAll(jsonSchema.getExtendedSchemaUris());
        schema.getBaseSchemaUris().addAll(baseSchemaUriSet);

        final List<Slot> slots = schema.getSlots();
        final Map<String, JsonSchema.Property> properties = jsonSchema.getProperties();

        for (final String name : properties.keySet()) {

            final JsonSchema.Property property = properties.get(name);

            final Slot slot = context.newModel(Slot.class);
            slot.setName(name);

            final String slotDescription = property.getValue(PropertyType.Description);
            if (slotDescription != null) {
                slot.setDescription(slotDescription);
            }
            final String slotTitle = property.getValue(PropertyType.Title);
            if (slotTitle != null) {
                slot.setTitle(slotTitle);
            }

            final Value value = generateValue(jsonSchema, property);

            if (value == null) {
                throw new SchemaGeneratorException("Failed to generate a Value for slot: " + name, null, this);
            }

            slot.setValue(value);

            slots.add(slot);

        }

        final List<JsonSchema.Link> links = jsonSchema.getLinks();
        for (final JsonSchema.Link jsonSchemaLink : links) {
            final Slot linkSlot = context.newModel(Slot.class);
            final URI linkRelationUri = jsonSchemaLink.getRelId();
            if (linkRelationUri == null) {
                continue;
            }

            final LinkRelation linkRelation = getContext().getApiLoader().loadLinkRelation(linkRelationUri);
            if (linkRelation == null) {
                continue;
            }
            final String linkSlotName = linkRelation.getUniqueName().getLocalName();
            if (linkSlotName == null) {
                continue;
            }

            linkSlot.setName(linkSlotName);

            final LinkValue linkValue = context.newModel(LinkValue.class);
            linkSlot.setValue(linkValue);
            linkValue.setLinkRelationUri(linkRelationUri);

            final URI targetSchemaUri = jsonSchemaLink.getTargetSchemaId();
            if (targetSchemaUri != null) {
                linkValue.setResponseSchemaUri(targetSchemaUri);
            }

            final URI paramSchemaUri = jsonSchemaLink.getSchemaId();
            linkValue.setRequestSchemaUri(paramSchemaUri);

            slots.add(linkSlot);
        }

        final ObjectNode rootNode = jsonSchema.getRootNode();
        if (rootNode.has(Schema.SLOT_NAME_KEY_SLOT_NAMES)) {
            final JsonNode keySlotNamesJsonNode = rootNode.get(Schema.SLOT_NAME_KEY_SLOT_NAMES);

            if (keySlotNamesJsonNode instanceof ArrayNode) {
                final ArrayNode keySlotNamesArrayNode = (ArrayNode) keySlotNamesJsonNode;
                final Iterator<JsonNode> elements = keySlotNamesArrayNode.elements();
                while (elements.hasNext()) {
                    final JsonNode keySlotNameJsonNode = elements.next();
                    final String keySlotName = keySlotNameJsonNode.asText();
                    schema.getKeySlotNames().add(keySlotName);
                }
            }
        }

        LOG.debug("Generated WRML Schema from JSON Schema (" + schema.getUri() + "):\n" + schema);

        return schema;

    }

    @SuppressWarnings("unchecked")
    public Schema generateSchema(final Prototype prototype) {

        final URI schemaUri = prototype.getSchemaUri();
        final Context context = getContext();

        final Schema schema = context.newModel(getSchemaLoader().getSchemaDimensions());
        schema.setUniqueName(prototype.getUniqueName());
        schema.setDescription(prototype.getDescription());

        final boolean isReadOnly = prototype.isReadOnly();
        if (isReadOnly) {
            schema.setReadOnly(true);
        }

        schema.setTitle(prototype.getTitle());
        schema.setThumbnailLocation(prototype.getThumbnailLocation());
        schema.setVersion(prototype.getVersion());
        schema.setTitleSlotName(prototype.getTitleSlotName());

        schema.setUri(schemaUri);

        final Set<URI> prototypeBaseSchemaUris = prototype.getDeclaredBaseSchemaUris();
        if (prototypeBaseSchemaUris != null) {
            schema.getBaseSchemaUris().addAll(prototypeBaseSchemaUris);
        }

        final SortedSet<String> prototypeKeySlotNames = prototype.getDeclaredKeySlotNames();
        if (prototypeKeySlotNames != null) {
            schema.getKeySlotNames().addAll(prototypeKeySlotNames);
        }

        final Set<String> prototypeComparableSlotNames = prototype.getComparableSlotNames();
        if (prototypeComparableSlotNames != null) {
            schema.getComparableSlotNames().addAll(prototypeComparableSlotNames);
        }

        final SortedSet<String> prototypeTags = prototype.getTags();
        if (prototypeTags != null) {
            schema.getTags().addAll(prototypeTags);
        }

        final List<Slot> slots = schema.getSlots();
        final SortedSet<String> prototypeAllSlotNames = prototype.getAllSlotNames();
        if (prototypeAllSlotNames != null) {

            for (final String name : prototypeAllSlotNames) {
                final ProtoSlot protoSlot = prototype.getProtoSlot(name);
                final URI slotDeclaredSchemaUri = protoSlot.getDeclaringSchemaUri();
                if (!schemaUri.equals(slotDeclaredSchemaUri)) {
                    continue;
                }

                final Slot slot = context.newModel(Slot.class);
                slot.setName(name);

                final SortedSet<String> aliases = protoSlot.getAliases();
                if (aliases != null) {
                    slot.getAliases().addAll(aliases);
                }

                slot.setDescription(protoSlot.getDescription());
                slot.setTitle(protoSlot.getTitle());

                final Type slotHeapValueType = protoSlot.getHeapValueType();
                final Value value = generateValue(slotHeapValueType, protoSlot);

                if (value == null) {
                    throw new SchemaGeneratorException("Failed to generate a Value for slot: " + protoSlot, null,
                            this);
                }

                if (protoSlot instanceof PropertyProtoSlot) {
                    final PropertyProtoSlot propertyProtoSlot = (PropertyProtoSlot) protoSlot;
                    final Object propertyDefaultValue = propertyProtoSlot.getDefaultValue();
                    final ValueType propertyValueType = propertyProtoSlot.getValueType();
                    final Object valueTypeDefaultValue = propertyValueType.getDefaultValue();
                    if (propertyDefaultValue != null && !propertyDefaultValue.equals(valueTypeDefaultValue)) {
                        value.setSlotValue(Value.SLOT_NAME_DEFAULT, propertyDefaultValue);
                    }

                    @SuppressWarnings("rawtypes")
                    final Set disallowedValues = propertyProtoSlot.getDisallowedValues();
                    if (disallowedValues != null && !disallowedValues.isEmpty()) {
                        propertyProtoSlot.getDisallowedValues().addAll(disallowedValues);
                    }

                    final Object divisibleByValue = propertyProtoSlot.getDivisibleByValue();
                    if (divisibleByValue != null) {
                        value.setSlotValue(NumericValue.SLOT_NAME_DIVISIBLE_BY, divisibleByValue);
                    }

                    final Integer maximumLength = propertyProtoSlot.getMaximumLength();
                    if (maximumLength != null) {
                        value.setSlotValue(TextValue.SLOT_NAME_MAXIMUM_LENGTH, maximumLength);
                    }

                    final Integer maximumSize = propertyProtoSlot.getMaximumSize();
                    if (maximumSize != null) {
                        value.setSlotValue(ListValue.SLOT_NAME_MAXIMUM_SIZE, maximumSize);
                    }

                    final Object maximumValue = propertyProtoSlot.getMaximumValue();
                    if (maximumValue != null) {
                        value.setSlotValue(NumericValue.SLOT_NAME_MAXIMUM, maximumValue);
                    }

                    final Integer minimumLength = propertyProtoSlot.getMinimumLength();
                    if (minimumLength != null) {
                        value.setSlotValue(TextValue.SLOT_NAME_MINIMUM_LENGTH, minimumLength);
                    }

                    final Integer minimumSize = propertyProtoSlot.getMinimumSize();
                    if (minimumSize != null) {
                        value.setSlotValue(ListValue.SLOT_NAME_MINIMUM_SIZE, minimumSize);
                    }

                    final Object minimumValue = propertyProtoSlot.getMinimumValue();
                    if (minimumValue != null) {
                        value.setSlotValue(NumericValue.SLOT_NAME_MINIMUM, minimumValue);
                    }

                    final boolean isMultiline = propertyProtoSlot.isMultiline();
                    if (isMultiline) {
                        value.setSlotValue(TextValue.SLOT_NAME_MULTILINE, isMultiline);
                    }

                    if (slotHeapValueType instanceof Class<?>
                            && Set.class.isAssignableFrom((Class<?>) slotHeapValueType)) {
                        value.setSlotValue(ListValue.SLOT_NAME_ELEMENT_UNIQUENESS_CONSTRAINED, Boolean.TRUE);
                    }

                }

                slot.setValue(value);

                slots.add(slot);

            }
        }

        return schema;
    }

    /**
     * Generate a {@link JavaBytecodeClass} from the specified {@link Schema}.
     * <p/>
     * Like a *big* regex (regular expression), we can compile all of the
     * WRML schema metadata (as if it is a single *big* String input) into a
     * loaded Java class so that the rest of the WRML *runtime* can use
     * (Prototype-optimized) reflection to access WRML's type system.
     *
     * @param schema The Schema to represent as a Java class.
     * @return The Java class representation of the specified schema.
     */
    public JavaBytecodeClass generateSchemaInterface(final Schema schema) {

        /*
         * Create the simple POJO that will return the transformation
         * information. By the end of this method, this will be full of Java
         * bytecode-oriented information gleaned from this method's schema
         * parameter.
         */
        final JavaBytecodeClass javaBytecodeClass = new JavaBytecodeClass();
        final JavaBytecodeAnnotation wrmlAnnotation = new JavaBytecodeAnnotation(
                SchemaGenerator.ANNOTATION_INTERNAL_NAME_WRML);
        wrmlAnnotation.setAttributeValue(AnnotationParameterName.uniqueName.name(),
                schema.getUniqueName().getFullName());
        javaBytecodeClass.getAnnotations().add(wrmlAnnotation);

        final SortedSet<String> keySlotNameSet = new TreeSet<>();

        /*
         * If the schema declares any key slots, note them with a
         * class-level annotation.
         */
        final List<String> keySlotNames = schema.getKeySlotNames();
        if (keySlotNames != null && keySlotNames.size() > 0) {
            keySlotNameSet.addAll(keySlotNames);
        }

        if (!keySlotNameSet.isEmpty()) {
            final String[] keySlotNamesArray = new String[keySlotNameSet.size()];
            wrmlAnnotation.setAttributeValue(AnnotationParameterName.keySlotNames.name(),
                    keySlotNameSet.toArray(keySlotNamesArray));
        }

        /*
         * If the schema declares any comparable slots, note them with a
         * class-level annotation.
         */
        final List<String> comparableSlotNames = schema.getComparableSlotNames();
        if (comparableSlotNames != null && comparableSlotNames.size() > 0) {
            final String[] comparableSlotNamesArray = new String[comparableSlotNames.size()];
            wrmlAnnotation.setAttributeValue(AnnotationParameterName.comparableSlotNames.name(),
                    comparableSlotNames.toArray(comparableSlotNamesArray));
        }

        wrmlAnnotation.setAttributeValue(AnnotationParameterName.titleSlotName.name(), schema.getTitleSlotName());

        /*
         * In Java, all interfaces extend java.lang.Object, so this can
         * remain constant for Schema too.
         */
        javaBytecodeClass.setSuperName(SchemaGenerator.OBJECT_INTERNAL_NAME);

        /*
         * Go from schema id (URI) to internal Java class name. This is a
         * simple matter of stripping the leading forward slash from the
         * URI's path. Internally (in the bytecode), Java's class names use
         * forward slash (/) instead of full stop dots (.).
         */
        final URI schemaUri = schema.getUri();
        final String interfaceInternalName = uriToInternalTypeName(schemaUri);
        // if (schema.getUniqueName() == null)
        // {
        // schema.setUniqueName(new UniqueName(schemaUri.getPath()));
        // }

        javaBytecodeClass.setInternalName(interfaceInternalName);

        /*
         * Add the class-level Description annotation to capture the
         * schema's description.
         */
        final String schemaDescription = schema.getDescription();
        if (schemaDescription != null && !schemaDescription.trim().isEmpty()) {
            final JavaBytecodeAnnotation schemaDescriptionAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_DESCRIPTION);
            schemaDescriptionAnnotation.setAttributeValue(AnnotationParameterName.value.name(), schemaDescription);
            javaBytecodeClass.getAnnotations().add(schemaDescriptionAnnotation);
        }

        String schemaTitle = schema.getTitle();
        if (schemaTitle == null || schemaTitle.trim().isEmpty()) {
            schemaTitle = schema.getUniqueName().getLocalName();
        }

        final JavaBytecodeAnnotation schemaTitleAnnotation = new JavaBytecodeAnnotation(
                SchemaGenerator.ANNOTATION_INTERNAL_NAME_TITLE);
        schemaTitleAnnotation.setAttributeValue(AnnotationParameterName.value.name(), schemaTitle);
        javaBytecodeClass.getAnnotations().add(schemaTitleAnnotation);

        final URI schemaThumbnailImageLocation = schema.getThumbnailLocation();
        if (schemaThumbnailImageLocation != null) {
            final JavaBytecodeAnnotation schemaThumbnailImageAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_THUMBNAIL_IMAGE);
            schemaThumbnailImageAnnotation.setAttributeValue(AnnotationParameterName.value.name(),
                    schemaThumbnailImageLocation.toString());
            javaBytecodeClass.getAnnotations().add(schemaThumbnailImageAnnotation);
        }

        boolean isAggregate = false;

        /*
         * Turn the schema's base schema list into our Java class's base
         * (aka extended) interfaces.
         */
        final List<URI> baseSchemaUris = schema.getBaseSchemaUris();
        for (final URI baseSchemaUri : baseSchemaUris) {
            final String baseSchemaInternalName = uriToInternalTypeName(baseSchemaUri);
            javaBytecodeClass.getInterfaces().add(baseSchemaInternalName);

            if (!isAggregate && getSchemaLoader().getAggregateDocumentSchemaUri().equals(baseSchemaUri)) {
                isAggregate = true;

                final List<Slot> slots = schema.getSlots();
                for (final Slot slot : slots) {
                    final Value value = slot.getValue();
                    if (!(value instanceof LinkValue || value instanceof ModelValue
                            || value instanceof MultiSelectValue)) {
                        keySlotNameSet.add(slot.getName());
                    }
                }
            }
        }

        // Add the Model base interface
        javaBytecodeClass.getInterfaces().add(SchemaGenerator.MODEL_INTERFACE_INTERNAL_NAME);

        /*
         * Add the class-level Tags annotation to capture the schema's tags.
         */
        final List<String> schemaTags = schema.getTags();
        if (schemaTags != null && schemaTags.size() > 0) {
            final JavaBytecodeAnnotation tagsAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_TAGS);
            final String[] tagsArray = new String[schemaTags.size()];
            tagsAnnotation.setAttributeValue(AnnotationParameterName.value.name(), schemaTags.toArray(tagsArray));
            javaBytecodeClass.getAnnotations().add(tagsAnnotation);
        }

        final Long schemaVersion = schema.getVersion();
        if (schemaVersion != null) {
            final JavaBytecodeAnnotation versionAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_VERSION);
            versionAnnotation.setAttributeValue(AnnotationParameterName.value.name(), schemaVersion);
            javaBytecodeClass.getAnnotations().add(versionAnnotation);
        }

        final Boolean maybeReadOnly = schema.isReadOnly();
        if (maybeReadOnly != null && maybeReadOnly) {
            final JavaBytecodeAnnotation readOnlyAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_READ_ONLY);
            javaBytecodeClass.getAnnotations().add(readOnlyAnnotation);
        }

        /*
         * Generate the interface method signatures.
         */
        generateSchemaInterfaceMethods(schema, javaBytecodeClass, isAggregate);

        // TODO: "Open slots" with signatures. Track the open slots via
        // the JavaBytecode types.

        //
        // TODO: The signature will need to be changed for generics:
        // Example:
        //
        // Java: public interface Test<T extends List<?>> extends List<T>
        // Class File: public abstract interface org.wrml.schema.Test
        // extends java.util.List
        // Signature:
        // <T::Ljava/util/List<*>;>Ljava/lang/Object;Ljava/util/List<TT;>;
        //

        javaBytecodeClass.setSignature(null);

        generateSchemaInterfaceBytecode(javaBytecodeClass);
        return javaBytecodeClass;
    }

    public Context getContext() {

        return getSchemaLoader().getContext();
    }

    public SchemaLoader getSchemaLoader() {

        return _SchemaLoader;
    }

    private void addAnnotations(final JavaBytecodeMethod method, final JavaBytecodeAnnotation... annotations) {

        if (annotations != null && annotations.length > 0) {
            final List<JavaBytecodeAnnotation> methodAnnotations = method.getAnnotations();
            for (final JavaBytecodeAnnotation annotation : annotations) {
                if (annotation != null) {
                    methodAnnotations.add(annotation);
                }
            }
        }
    }

    private String generateArgs(final String... args) {

        if (args == null || args.length == 0) {
            return SchemaGenerator.EMPTY_PARENTHESES;
        } else {
            StringBuilder argsStringBuilder = null;

            for (final String arg : args) {
                if (arg == null) {
                    continue;
                }

                if (argsStringBuilder == null) {
                    argsStringBuilder = new StringBuilder();
                    argsStringBuilder.append(SchemaGenerator.OPEN_PARENTHESIS);
                }

                argsStringBuilder.append(arg);

            }

            if (argsStringBuilder != null) {
                argsStringBuilder.append(SchemaGenerator.CLOSE_PARENTHESIS);
                return argsStringBuilder.toString();
            }

        }

        return null;
    }

    private String generateArgsDescriptor(final JavaBytecodeType... argumentTypes) {

        if (argumentTypes == null || argumentTypes.length == 0) {
            return generateArgs((String[]) null);
        }

        final List<String> argList = new ArrayList<String>();
        for (int i = 0; i < argumentTypes.length; i++) {
            final JavaBytecodeType argType = argumentTypes[i];
            if (argType != null) {
                argList.add(argType.getDescriptor());
            }
        }

        if (argList.size() > 0) {
            String[] args = new String[argList.size()];
            args = argList.toArray(args);
            return generateArgs(args);
        }

        return generateArgs((String[]) null);
    }

    private String generateArgsSignature(final JavaBytecodeType... argumentTypes) {

        if (argumentTypes == null || argumentTypes.length == 0) {
            return generateArgs((String[]) null);
        }

        final List<String> argList = new ArrayList<String>();
        for (int i = 0; i < argumentTypes.length; i++) {
            final JavaBytecodeType argType = argumentTypes[i];
            if (argType != null) {
                argList.add(argType.getGenericSignature());
            }
        }

        if (argList.size() > 0) {
            String[] args = new String[argList.size()];
            args = argList.toArray(args);
            return generateArgs(args);
        }

        return null;
    }

    private void generateBeanAccessorMethods(final JavaBytecodeClass javaBytecodeClass, final Schema schema,
            final boolean isAggregate, final Slot slot) {

        final String slotName = slot.getName();
        final Value slotValue = slot.getValue();
        final JavaBytecodeType type = getSlotType(javaBytecodeClass, schema, isAggregate, slot);

        /*
         * Following JavaBean property conventions, the suffix (end) of the
         * property accessor's method name will be the WRML schema slot's
         * name as mixed upper case.
         */
        final String methodNameSuffix = StringUtils.capitalize(StringUtils.deleteWhitespace(slotName));
        final String readMethodNamePrefix = (slotValue instanceof BooleanValue) ? JavaBean.IS : JavaBean.GET;
        final String readMethodName = readMethodNamePrefix + methodNameSuffix;

        JavaBytecodeAnnotation aliasAnnotation = null;
        JavaBytecodeAnnotation descriptionAnnotation = null;
        JavaBytecodeAnnotation titleAnnotation = null;
        JavaBytecodeAnnotation defaultValueAnnotation = null;

        final List<String> aliases = slot.getAliases();
        if (aliases != null && aliases.size() > 0) {
            aliasAnnotation = new JavaBytecodeAnnotation(SchemaGenerator.ANNOTATION_INTERNAL_NAME_ALIASES);
            final String[] aliasesArray = new String[aliases.size()];
            aliasAnnotation.setAttributeValue(AnnotationParameterName.value.name(), aliases.toArray(aliasesArray));
        }

        final String descriptionString = slot.getDescription();
        if (descriptionString != null && !descriptionString.isEmpty()) {
            descriptionAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_DESCRIPTION);
            descriptionAnnotation.setAttributeValue(AnnotationParameterName.value.name(), descriptionString);
        }

        final String titleString = slot.getTitle();
        if (titleString != null && !titleString.isEmpty()) {
            titleAnnotation = new JavaBytecodeAnnotation(SchemaGenerator.ANNOTATION_INTERNAL_NAME_TITLE);
            titleAnnotation.setAttributeValue(AnnotationParameterName.value.name(), titleString);
        }

        final String defaultValueString = getDefaultValueString(slotValue);
        if (defaultValueString != null && !defaultValueString.isEmpty()) {
            defaultValueAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_DEFAULT_VALUE);
            defaultValueAnnotation.setAttributeValue(AnnotationParameterName.value.name(), defaultValueString);
        }

        JavaBytecodeAnnotation disallowedValuesAnnotation = null;
        JavaBytecodeAnnotation divisibleByAnnotation = null;
        JavaBytecodeAnnotation maximumLengthAnnotation = null;
        JavaBytecodeAnnotation maximumSizeAnnotation = null;
        JavaBytecodeAnnotation maximumValueAnnotation = null;
        JavaBytecodeAnnotation minimumLengthAnnotation = null;
        JavaBytecodeAnnotation minimumSizeAnnotation = null;
        JavaBytecodeAnnotation minimumValueAnnotation = null;
        JavaBytecodeAnnotation multilineAnnotation = null;

        JavaBytecodeAnnotation collectionSlotAnnotation = null;

        if (slotValue.containsSlotValue(Value.SLOT_NAME_DISALLOWED_VALUES)) {

            final List<?> disallowedValues = (List<?>) slotValue.getSlotValue(Value.SLOT_NAME_DISALLOWED_VALUES);
            if (disallowedValues != null && disallowedValues.size() > 0) {
                disallowedValuesAnnotation = new JavaBytecodeAnnotation(
                        SchemaGenerator.ANNOTATION_INTERNAL_NAME_DISALLOWED_VALUES);

                final String[] values = new String[disallowedValues.size()];

                for (int i = 0; i < values.length; i++) {
                    values[i] = String.valueOf(disallowedValues.get(i));
                }

                disallowedValuesAnnotation.setAttributeValue(AnnotationParameterName.value.name(), values);

            }

        }

        if (slotValue.containsSlotValue(TextValue.SLOT_NAME_MULTILINE) && slotValue instanceof TextValue) {
            final boolean isMultiline = (boolean) slotValue.getSlotValue(TextValue.SLOT_NAME_MULTILINE);
            if (isMultiline) {
                multilineAnnotation = new JavaBytecodeAnnotation(
                        SchemaGenerator.ANNOTATION_INTERNAL_NAME_MULTILINE);
            }
        }

        if (slotValue.containsSlotValue(NumericValue.SLOT_NAME_DIVISIBLE_BY)
                && (slotValue instanceof IntegerValue || slotValue instanceof LongValue)) {
            divisibleByAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_DIVISIBLE_BY_VALUE);
            divisibleByAnnotation.setAttributeValue(AnnotationParameterName.value.name(),
                    String.valueOf(slotValue.getSlotValue(NumericValue.SLOT_NAME_DIVISIBLE_BY)));

        }

        // Maximum values

        if (slotValue.containsSlotValue(TextValue.SLOT_NAME_MAXIMUM_LENGTH) && slotValue instanceof TextValue) {
            maximumLengthAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_MAXIMUM_LENGTH);
            maximumLengthAnnotation.setAttributeValue(AnnotationParameterName.value.name(),
                    (int) slotValue.getSlotValue(TextValue.SLOT_NAME_MAXIMUM_LENGTH));

        } else if (slotValue.containsSlotValue(ListValue.SLOT_NAME_MAXIMUM_SIZE)
                && slotValue instanceof ListValue) {
            maximumSizeAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_MAXIMUM_SIZE);
            maximumSizeAnnotation.setAttributeValue(AnnotationParameterName.value.name(),
                    (int) slotValue.getSlotValue(ListValue.SLOT_NAME_MAXIMUM_SIZE));

        } else if (slotValue.containsSlotValue(NumericValue.SLOT_NAME_MAXIMUM)
                && slotValue instanceof NumericValue) {
            maximumValueAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_MAXIMUM_VALUE);
            maximumValueAnnotation.setAttributeValue(AnnotationParameterName.value.name(),
                    String.valueOf(slotValue.getSlotValue(NumericValue.SLOT_NAME_MAXIMUM)));

            maximumValueAnnotation.setAttributeValue(AnnotationParameterName.exclusive.name(),
                    ((NumericValue) slotValue).isExclusiveMaximum());

        }

        // Minimum values

        if (slotValue.containsSlotValue(TextValue.SLOT_NAME_MINIMUM_LENGTH) && slotValue instanceof TextValue) {
            minimumLengthAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_MINIMUM_LENGTH);
            minimumLengthAnnotation.setAttributeValue(AnnotationParameterName.value.name(),
                    (int) slotValue.getSlotValue(TextValue.SLOT_NAME_MINIMUM_LENGTH));

        } else if (slotValue.containsSlotValue(ListValue.SLOT_NAME_MINIMUM_SIZE)
                && slotValue instanceof ListValue) {
            minimumSizeAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_MINIMUM_SIZE);
            minimumSizeAnnotation.setAttributeValue(AnnotationParameterName.value.name(),
                    (int) slotValue.getSlotValue(ListValue.SLOT_NAME_MINIMUM_SIZE));

        } else if (slotValue.containsSlotValue(NumericValue.SLOT_NAME_MINIMUM)
                && slotValue instanceof NumericValue) {
            minimumValueAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_MINIMUM_VALUE);
            minimumValueAnnotation.setAttributeValue(AnnotationParameterName.value.name(),
                    String.valueOf(slotValue.getSlotValue(NumericValue.SLOT_NAME_MINIMUM)));

            minimumValueAnnotation.setAttributeValue(AnnotationParameterName.exclusive.name(),
                    ((NumericValue) slotValue).isExclusiveMinimum());

        }

        if (slotValue instanceof CollectionValue) {

            final CollectionValue collectionValue = (CollectionValue) slotValue;
            final Slot elementSlot = collectionValue.getElementSlot();

            if (elementSlot == null) {
                throw new SchemaGeneratorException("The collection value: " + collectionValue
                        + " does not define an element slot. Collection values must have an element type of "
                        + ModelValue.class, null, this);
            }

            final Value elementValue = collectionValue.getElementSlot().getValue();
            if (!(elementValue instanceof ModelValue)) {
                throw new SchemaGeneratorException("The collection value: " + collectionValue
                        + " does not define a model-based element slot. Collection values must have an element type of "
                        + ModelValue.class, null, this);
            }

            collectionSlotAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_COLLECTION_SLOT);

            final URI linkRelationUri = collectionValue.getLinkRelationUri();
            collectionSlotAnnotation.setAttributeValue(AnnotationParameterName.linkRelationUri.name(),
                    linkRelationUri.toString());

            final Integer limit = collectionValue.getLimit();
            if (limit != null) {
                if (limit < 1) {
                    throw new SchemaGeneratorException(
                            "The collection value: " + collectionValue + " defines an invalid limit: " + limit,
                            null, this);
                }

                collectionSlotAnnotation.setAttributeValue(AnnotationParameterName.limit.name(), limit.toString());
            }

            final List<CollectionValueSearchCriterion> andedCriterionList = collectionValue.getAnd();
            if (andedCriterionList.size() > 0) {
                final JavaBytecodeAnnotation[] andCriterionArray = generateCollectionSlotCriterionArray(
                        andedCriterionList);
                collectionSlotAnnotation.setAttributeValue(AnnotationParameterName.and.name(), andCriterionArray);
            }

            final List<CollectionValueSearchCriterion> oredCriterionList = collectionValue.getOr();
            if (oredCriterionList.size() > 0) {
                final JavaBytecodeAnnotation[] orCriterionArray = generateCollectionSlotCriterionArray(
                        oredCriterionList);
                collectionSlotAnnotation.setAttributeValue(AnnotationParameterName.or.name(), orCriterionArray);
            }

        }

        //
        // Create an object to describe the *read* access method (aka
        // "getter" method) that needs
        // to be generated.
        //
        final JavaBytecodeMethod readMethod = generateMethod(readMethodName, type, aliasAnnotation,
                descriptionAnnotation, titleAnnotation, defaultValueAnnotation, disallowedValuesAnnotation,
                divisibleByAnnotation, maximumValueAnnotation, minimumValueAnnotation, maximumLengthAnnotation,
                minimumLengthAnnotation, maximumSizeAnnotation, minimumSizeAnnotation, multilineAnnotation,
                collectionSlotAnnotation);

        javaBytecodeClass.getMethods().add(readMethod);

        if (aliases != null && aliases.size() > 0) {
            for (final String alias : aliases) {
                if (alias == null) {
                    continue;
                }

                final String aliasMethodNameSuffix = StringUtils.capitalize(StringUtils.deleteWhitespace(alias));
                final String aliasReadMethodName = readMethodNamePrefix + aliasMethodNameSuffix;

                final JavaBytecodeMethod aliasReadMethod = generateMethod(aliasReadMethodName, type);

                javaBytecodeClass.getMethods().add(aliasReadMethod);

            }
        }

        if (slotValue instanceof MaybeReadOnly) {

            final Boolean maybeReadOnly = ((MaybeReadOnly) slotValue).isReadOnly();

            if (maybeReadOnly == null || !maybeReadOnly) {

                final String writeMethodName = JavaBean.SET + methodNameSuffix;

                JavaBytecodeAnnotation nonNullAnnotation = null; // ;-)
                if (slotValue instanceof MaybeRequired) {
                    final Boolean maybeRequired = ((MaybeRequired) slotValue).isRequired();
                    final boolean required = (maybeRequired != null && maybeRequired);
                    if (required && !(slotValue instanceof NumericValue) && !(slotValue instanceof BooleanValue)) {
                        nonNullAnnotation = new JavaBytecodeAnnotation(
                                SchemaGenerator.ANNOTATION_INTERNAL_NAME_NON_NULL);
                    }
                }

                //
                // Create an object to describe the *write* access method
                // (aka "setter" method) that needs
                // to be generated.
                //
                final JavaBytecodeMethod writeMethod = generateMethod(writeMethodName, type, type,
                        nonNullAnnotation);
                javaBytecodeClass.getMethods().add(writeMethod);

                if (aliases != null && aliases.size() > 0) {
                    for (final String alias : aliases) {
                        if (alias == null) {
                            continue;
                        }

                        final String aliasMethodNameSuffix = StringUtils
                                .capitalize(StringUtils.deleteWhitespace(alias));
                        final String aliasWriteMethodName = JavaBean.SET + aliasMethodNameSuffix;
                        final JavaBytecodeMethod aliasWriteMethod = generateMethod(aliasWriteMethodName, type,
                                type);
                        javaBytecodeClass.getMethods().add(aliasWriteMethod);
                    }
                }
            }
        }

    }

    private JavaBytecodeAnnotation[] generateCollectionSlotCriterionArray(
            final List<CollectionValueSearchCriterion> criterionList) {

        final JavaBytecodeAnnotation[] criterionArray = new JavaBytecodeAnnotation[criterionList.size()];
        for (int i = 0; i < criterionArray.length; i++) {

            final CollectionValueSearchCriterion criterion = criterionList.get(i);
            final JavaBytecodeAnnotation collectionSlotCriterionAnnotation = new JavaBytecodeAnnotation(
                    SchemaGenerator.ANNOTATION_INTERNAL_NAME_COLLECTION_SLOT_CRITERION);
            criterionArray[i] = collectionSlotCriterionAnnotation;

            final String referenceSlot = criterion.getReferenceSlot();
            collectionSlotCriterionAnnotation.setAttributeValue(AnnotationParameterName.referenceSlot.name(),
                    referenceSlot);

            final ComparisonOperator operator = criterion.getOperator();
            collectionSlotCriterionAnnotation.setAttributeValue(AnnotationParameterName.operator.name(), operator);

            final String linkValueSource = criterion.getValueSource();
            final ValueSourceType linkValueSourceType = criterion.getValueSourceType();

            if (operator != ComparisonOperator.exists && operator != ComparisonOperator.notExists
                    && linkValueSource != null) {
                collectionSlotCriterionAnnotation.setAttributeValue(AnnotationParameterName.valueSource.name(),
                        linkValueSource);
                collectionSlotCriterionAnnotation.setAttributeValue(AnnotationParameterName.valueSourceType.name(),
                        linkValueSourceType);
            }

            final String regex = criterion.getRegex();
            if (operator == ComparisonOperator.regex && regex != null) {
                collectionSlotCriterionAnnotation.setAttributeValue(AnnotationParameterName.regex.name(), regex);
            }

        }

        return criterionArray;
    }

    private List<CollectionValueSearchCriterion> generateCollectionValueSearchCriterionList(
            final List<ProtoSearchCriterion> protoSearchCriterionList) {

        final int listSize = (protoSearchCriterionList != null) ? protoSearchCriterionList.size() : 0;
        final List<CollectionValueSearchCriterion> criterionList = new ArrayList<>(listSize);

        final Context context = getContext();
        if (protoSearchCriterionList != null) {
            for (final ProtoSearchCriterion protoSearchCriterion : protoSearchCriterionList) {

                final CollectionValueSearchCriterion searchCriterion = context
                        .newModel(CollectionValueSearchCriterion.class);

                final ComparisonOperator operator = protoSearchCriterion.getComparisonOperator();
                searchCriterion.setOperator(operator);

                final ProtoValueSource protoValueSource = protoSearchCriterion.getProtoValueSource();
                searchCriterion.setReferenceSlot(protoValueSource.getReferenceSlot());

                if (operator != ComparisonOperator.exists && operator != ComparisonOperator.notExists) {
                    searchCriterion.setValueSourceType(protoValueSource.getValueSourceType());
                    searchCriterion.setValueSource(protoValueSource.getValueSource());

                }

                if (operator == ComparisonOperator.regex) {
                    searchCriterion.setRegex(protoSearchCriterion.getRegex());
                }

                criterionList.add(searchCriterion);
            }
        }

        return criterionList;
    }

    /**
     * Generates the method associated with a Link slot value ({@link LinkValue}).
     *
     * @param javaBytecodeClass The {@link JavaBytecodeClass} representing the Schema interface being generated.
     * @param isAggregate       <code>true</code> if the {@link Schema} that owns the link {@link Slot} is an {@link org.wrml.model.rest.AggregateDocument}.
     * @param slot              The slot holding the LinkValue to use as a template for a set of generated Java methods.
     */
    private void generateLinkMethod(final JavaBytecodeClass javaBytecodeClass, final boolean isAggregate,
            final Slot slot) {

        JavaBytecodeAnnotation aliasAnnotation = null;

        final List<String> aliases = slot.getAliases();
        if (aliases != null && aliases.size() > 0) {
            aliasAnnotation = new JavaBytecodeAnnotation(SchemaGenerator.ANNOTATION_INTERNAL_NAME_ALIASES);
            final String[] aliasesArray = new String[aliases.size()];
            aliasAnnotation.setAttributeValue(AnnotationParameterName.value.name(), aliases.toArray(aliasesArray));
        }

        final JavaBytecodeAnnotation linkSlotAnnotation = new JavaBytecodeAnnotation(
                SchemaGenerator.ANNOTATION_INTERNAL_NAME_LINK_SLOT);

        final LinkValue linkValue = (LinkValue) slot.getValue();

        final URI linkRelationUri = linkValue.getLinkRelationUri();
        if (linkRelationUri == null) {
            throw new SchemaGeneratorException("The link must specify a URI value in its \"linkRelationUri\" slot.",
                    null, this);
        }

        final String linkRelationUriString = linkRelationUri.toASCIIString();
        if (linkRelationUriString == null || linkRelationUriString.isEmpty()) {
            throw new SchemaGeneratorException("The link must specify a URI value in its \"linkRelationUri\" slot.",
                    null, this);
        }

        linkSlotAnnotation.setAttributeValue(AnnotationParameterName.linkRelationUri.name(), linkRelationUriString);

        final ApiLoader apiLoader = getContext().getApiLoader();
        final LinkRelation linkRelation = apiLoader.loadLinkRelation(linkRelationUri);
        if (linkRelation == null) {
            throw new SchemaGeneratorException("Could not get the LinkRelation with URI: " + linkRelationUri, null,
                    this);
        }

        final String slotName = slot.getName();

        final String linkSlotName = StringUtils.deleteWhitespace(slotName);
        String linkMethodName = linkSlotName;

        final Method method = linkRelation.getMethod();
        linkSlotAnnotation.setAttributeValue(AnnotationParameterName.method.name(), method);

        final URI linkValueResponseSchemaUri = linkValue.getResponseSchemaUri();
        final URI linkRelationResponseSchemaUri = linkRelation.getResponseSchemaUri();
        final URI methodReturnSchemaUri = (linkValueResponseSchemaUri != null) ? linkValueResponseSchemaUri
                : linkRelationResponseSchemaUri;
        final JavaBytecodeType methodReturnType = javaBytecodeTypeForSchemaUri(methodReturnSchemaUri);

        final URI linkValueRequestSchemaUri = linkValue.getRequestSchemaUri();
        final URI linkRelationRequestSchemaUri = linkRelation.getRequestSchemaUri();
        final URI methodParameterSchemaUri = (linkValueRequestSchemaUri != null) ? linkValueRequestSchemaUri
                : linkRelationRequestSchemaUri;

        final JavaBytecodeMethod linkMethod;
        if (methodParameterSchemaUri != null) {
            // Link method accepts a parameter
            final JavaBytecodeType methodParameterType = javaBytecodeTypeForSchemaUri(methodParameterSchemaUri);
            linkMethod = generateMethod(linkMethodName, methodReturnType, methodParameterType, aliasAnnotation,
                    linkSlotAnnotation);
        } else {
            // A zero argument link method
            if (method == Method.Get) {
                // Links with GET semantics are named with a pattern: get{LinkSlotName}()
                linkMethodName = JavaBean.GET + StringUtils.capitalize(linkMethodName);
            }

            linkMethod = generateMethod(linkMethodName, methodReturnType, aliasAnnotation, linkSlotAnnotation);
        }

        boolean embedded = (linkValue.isEmbedded() || isAggregate) && (method == Method.Get);
        linkSlotAnnotation.setAttributeValue(AnnotationParameterName.embedded.name(), embedded);

        List<LinkValueBinding> linkValueBindings = linkValue.getBindings();
        if (!linkValueBindings.isEmpty()) {

            final JavaBytecodeAnnotation[] bindingsArray = new JavaBytecodeAnnotation[linkValueBindings.size()];
            for (int i = 0; i < bindingsArray.length; i++) {

                final LinkValueBinding linkValueBinding = linkValueBindings.get(i);
                final String referenceSlot = linkValueBinding.getReferenceSlot();
                final String linkValueSource = linkValueBinding.getValueSource();
                final ValueSourceType linkValueSourceType = linkValueBinding.getValueSourceType();

                final JavaBytecodeAnnotation linkSlotBindingAnnotation = new JavaBytecodeAnnotation(
                        SchemaGenerator.ANNOTATION_INTERNAL_NAME_LINK_SLOT_BINDING);
                linkSlotBindingAnnotation.setAttributeValue(AnnotationParameterName.referenceSlot.name(),
                        referenceSlot);
                linkSlotBindingAnnotation.setAttributeValue(AnnotationParameterName.valueSource.name(),
                        linkValueSource);
                linkSlotBindingAnnotation.setAttributeValue(AnnotationParameterName.valueSourceType.name(),
                        linkValueSourceType);

                bindingsArray[i] = linkSlotBindingAnnotation;
            }

            linkSlotAnnotation.setAttributeValue(AnnotationParameterName.bindings.name(), bindingsArray);

        }

        javaBytecodeClass.getMethods().add(linkMethod);

    }

    private List<LinkValueBinding> generateLinkValueBindingList(final LinkProtoSlot linkProtoSlot) {

        final Context context = getContext();
        final Map<String, ProtoValueSource> linkSlotBindings = linkProtoSlot.getLinkSlotBindings();
        final List<LinkValueBinding> linkValueBindingList = new ArrayList<>(linkSlotBindings.size());
        final Collection<ProtoValueSource> protoValueSources = linkSlotBindings.values();
        for (final ProtoValueSource protoValueSource : protoValueSources) {
            final LinkValueBinding linkValueBinding = context.newModel(LinkValueBinding.class);
            linkValueBindingList.add(linkValueBinding);

            linkValueBinding.setReferenceSlot(protoValueSource.getReferenceSlot());
            linkValueBinding.setValueSourceType(protoValueSource.getValueSourceType());
            linkValueBinding.setValueSource(protoValueSource.getValueSource());
        }

        return linkValueBindingList;

    }

    private JavaBytecodeMethod generateMethod(final String name, final JavaBytecodeType returnType,
            final JavaBytecodeAnnotation... annotations) {

        return generateMethod(name, returnType, (JavaBytecodeType) null, annotations);
    }

    private JavaBytecodeMethod generateMethod(final String name, final JavaBytecodeType returnType,
            final JavaBytecodeType argumentType, final JavaBytecodeAnnotation... annotations) {

        final JavaBytecodeMethod method = new JavaBytecodeMethod();
        method.setName(StringUtils.deleteWhitespace(name));
        method.setDescriptor(generateMethodDescriptor(returnType, argumentType));
        method.setSignature(generateMethodSignature(returnType, argumentType));
        addAnnotations(method, annotations);
        return method;
    }

    private String generateMethodDescriptor(final JavaBytecodeType returnType,
            final JavaBytecodeType argumentType) {

        final String returnTypeDescriptor = generateReturnTypeDescriptor(returnType);
        final String argsDescriptor = generateArgsDescriptor(argumentType);
        final String methodDescriptor = argsDescriptor + returnTypeDescriptor;
        return methodDescriptor;
    }

    private String generateMethodDescriptor(final JavaBytecodeType returnType,
            final JavaBytecodeType[] argumentTypes) {

        final String returnTypeDescriptor = generateReturnTypeDescriptor(returnType);
        final String argsDescriptor = generateArgsDescriptor(argumentTypes);
        final String methodDescriptor = argsDescriptor + returnTypeDescriptor;
        return methodDescriptor;
    }

    private String generateMethodSignature(final JavaBytecodeType returnType, final JavaBytecodeType argumentType) {

        final String returnTypeSignature = generateReturnTypeSignature(returnType);
        final String argsSignature = generateArgsSignature(argumentType);
        final String methodSignature = generateMethodSignature(returnTypeSignature, argsSignature);
        return methodSignature;

    }

    private String generateMethodSignature(final JavaBytecodeType returnType,
            final JavaBytecodeType[] argumentTypes) {

        final String returnTypeSignature = generateReturnTypeSignature(returnType);
        final String argsSignature = generateArgsSignature(argumentTypes);
        final String methodSignature = generateMethodSignature(returnTypeSignature, argsSignature);
        return methodSignature;
    }

    private String generateMethodSignature(final String returnTypeSignature, final String argsSignature) {

        if (returnTypeSignature == null && argsSignature == null) {
            return null;
        }

        if (returnTypeSignature == null && argsSignature != null) {
            return argsSignature + JavaBytecodeType.VoidPrimitiveBytecodeType.getDescriptor();
        } else if (returnTypeSignature != null && argsSignature == null) {
            return SchemaGenerator.EMPTY_PARENTHESES + returnTypeSignature;
        } else {
            return argsSignature + returnTypeSignature;
        }

    }

    private String generateReturnTypeDescriptor(final JavaBytecodeType returnType) {

        String returnTypeDescriptor = null;
        if (returnType != null) {
            returnTypeDescriptor = returnType.getDescriptor();
        }

        if (returnTypeDescriptor == null) {
            returnTypeDescriptor = JavaBytecodeType.VoidPrimitiveBytecodeType.getDescriptor();
        }

        return returnTypeDescriptor;
    }

    private String generateReturnTypeSignature(final JavaBytecodeType returnType) {

        final String returnTypeSignature;
        if (returnType == null) {
            returnTypeSignature = null;
        } else {
            returnTypeSignature = returnType.getGenericSignature();
        }
        return returnTypeSignature;
    }

    /**
     * Handles the actual JVM bytecode generation.
     *
     * @param classFile The Java class file information used to drive the bytecode
     *                  generation.
     */
    private void generateSchemaInterfaceBytecode(final JavaBytecodeClass classFile) {

        final ClassWriter classVisitor = new ClassWriter(0);

        final List<String> interfaces = classFile.getInterfaces();
        String[] interfaceNames = new String[interfaces.size()];
        interfaceNames = (interfaceNames.length > 0) ? interfaces.toArray(interfaceNames) : null;

        // Generate the interface declaration
        classVisitor.visit(SchemaGenerator.JVM_VERSION, SchemaGenerator.INTERFACE_ACCESS,
                classFile.getInternalName(), classFile.getSignature(), classFile.getSuperName(), interfaceNames);

        for (final JavaBytecodeAnnotation annotation : classFile.getAnnotations()) {
            visitAnnotation(annotation, classVisitor, null, null, null);
        }

        final List<JavaBytecodeMethod> methods = classFile.getMethods();

        for (final JavaBytecodeMethod method : methods) {

            final List<String> exceptions = method.getExceptions();
            String[] exceptionNames = new String[exceptions.size()];
            exceptionNames = (exceptionNames.length > 0) ? exceptions.toArray(exceptionNames) : null;

            final MethodVisitor methodVisitor = classVisitor.visitMethod(SchemaGenerator.INTERFACE_METHOD_ACCESS,
                    method.getName(), method.getDescriptor(), method.getSignature(), exceptionNames);

            // If we weren't simply generating Java interfaces, things would
            // get more _advanced_ right here.

            for (final JavaBytecodeAnnotation annotation : method.getAnnotations()) {
                visitAnnotation(annotation, null, methodVisitor, null, null);
            }

            methodVisitor.visitEnd();
        }

        // Finish the code generation
        classVisitor.visitEnd();

        final byte[] bytecode = classVisitor.toByteArray();
        classFile.setBytecode(bytecode);
    }

    /*
     * For each WRML Slot defined by the schema, generate a JavaBean
     * property in the class file.
     *
     * A JavaBean property is not formalized by the language but rather
     * exists via the pattern of a "getter" method and, for non-read only
     * (writable) properties, also a "setter" method. For more information
     * about the JavaBean property pattern, see:
     *
     * <a href="http://docs.oracle.com/javase/tutorial/javabeans/writing/properties.html">
     */
    private void generateSchemaInterfaceMethods(final Schema schema, final JavaBytecodeClass javaBytecodeClass,
            final boolean isAggregate) {

        final List<Slot> slots = schema.getSlots();
        for (final Slot slot : slots) {

            /*
             * WRML schema slots must be named using mixed lower case (aka
             * camel case) with no whitespace allowed.
             */
            final String slotName = slot.getName();

            if (_ModelJavaBean.getProperties().containsKey(slotName)) {

                // TODO: Filter other things like Java reserved words and
                // symbols

                throw new SchemaGeneratorException(
                        "Sorry but \"" + slotName + "\" is reserved, it is not legal within a Schema.", null, this);
            }

            if (slot != null && slot.getValue() instanceof LinkValue) {
                generateLinkMethod(javaBytecodeClass, isAggregate, slot);
            } else {
                generateBeanAccessorMethods(javaBytecodeClass, schema, isAggregate, slot);
            }
        }
    }

    private Value generateValue(final JsonSchema jsonSchema, final JsonSchema.Property property)
            throws SchemaGeneratorException {

        if (property == null) {
            throw new SchemaGeneratorException("Failed to generate a Value", null, this);
        }

        final URI schemaUri = jsonSchema.getId();
        if (schemaUri == null) {
            throw new SchemaGeneratorException("Failed to generate a Value", null, this);
        }

        final JsonType jsonType = property.getJsonType();
        if (jsonType == null) {
            throw new SchemaGeneratorException("Failed to generate a Value", null, this);
        }

        final Context context = getContext();
        final SchemaLoader schemaLoader = context.getSchemaLoader();

        final ValueType valueType = getValueType(jsonType);
        Value value = null;

        switch (valueType) {

        case Boolean: {
            final BooleanValue booleanValue = context.newModel(BooleanValue.class);

            final JsonNode defaultNode = property.getValueNode(PropertyType.Default);
            if (defaultNode != null) {
                booleanValue.setDefault(defaultNode.asBoolean());
            }

            value = booleanValue;
            break;
        }
        case Date: {
            break;
        }
        case Double: {
            final DoubleValue doubleValue = context.newModel(DoubleValue.class);

            final JsonNode defaultNode = property.getValueNode(PropertyType.Default);
            if (defaultNode != null) {
                doubleValue.setDefault(defaultNode.asDouble());
            }

            final JsonNode maximumNode = property.getValueNode(PropertyType.Maximum);
            if (maximumNode != null) {
                doubleValue.setMaximum(maximumNode.asDouble());
            }

            final JsonNode minimumNode = property.getValueNode(PropertyType.Minimum);
            if (minimumNode != null) {
                doubleValue.setMinimum(minimumNode.asDouble());
            }

            value = doubleValue;
            break;
        }
        case Integer: {
            final IntegerValue integerValue = context.newModel(IntegerValue.class);

            final JsonNode defaultNode = property.getValueNode(PropertyType.Default);
            if (defaultNode != null) {
                integerValue.setDefault(defaultNode.asInt());
            }

            final JsonNode divisibleByNode = property.getValueNode(PropertyType.DivisibleBy);
            if (divisibleByNode != null) {
                integerValue.setDivisibleBy(divisibleByNode.asInt());
            }

            final JsonNode multipleOfNode = property.getValueNode(PropertyType.MultipleOf);
            if (multipleOfNode != null) {
                integerValue.setDivisibleBy(multipleOfNode.asInt());
            }

            final JsonNode maximumNode = property.getValueNode(PropertyType.Maximum);
            if (maximumNode != null) {
                integerValue.setMaximum(maximumNode.asInt());
            }

            final JsonNode minimumNode = property.getValueNode(PropertyType.Minimum);
            if (minimumNode != null) {
                integerValue.setMinimum(minimumNode.asInt());
            }

            value = integerValue;
            break;
        }
        case Link:
            // NOTE: Falling through the Link switch case on purpose to treat same as Model
            // TODO: Revisit this design.
        case Model: {

            final ModelValue modelValue = context.newModel(ModelValue.class);
            final JsonSchemaLoader jsonSchemaLoader = schemaLoader.getJsonSchemaLoader();
            final JsonSchema modelJsonSchema;

            final URI baseSchemaUri;
            final JsonNode schemaIdNode = property.getValueNode(PropertyType.Id);
            if (schemaIdNode != null) {
                baseSchemaUri = schemaLoader.getDocumentSchemaUri();

                final URI modelSchemaUri = property.getValue(PropertyType.Id);
                if (modelSchemaUri != schemaUri) {
                    try {
                        modelJsonSchema = jsonSchemaLoader.load(modelSchemaUri);
                    } catch (final IOException e) {
                        throw new SchemaGeneratorException(e.getMessage(), e, this);
                    }
                } else {
                    modelJsonSchema = jsonSchema;
                }
            } else {

                baseSchemaUri = schemaLoader.getEmbeddedSchemaUri();

                String modelSchemaUriString = schemaUri.toString();

                if (modelSchemaUriString.endsWith(".json")) {
                    modelSchemaUriString = modelSchemaUriString.substring(0,
                            modelSchemaUriString.length() - ".json".length());
                }

                int innerClassNumber = 0;

                if (_InnerSchemaCountMap.containsKey(schemaUri)) {
                    innerClassNumber = _InnerSchemaCountMap.get(schemaUri);
                }

                innerClassNumber++;

                _InnerSchemaCountMap.put(schemaUri, innerClassNumber);

                final String annonymousInnerSchemaName = ANNONYMOUS_INNER_SCHEMA_PREFIX
                        + String.valueOf(innerClassNumber);

                modelSchemaUriString = modelSchemaUriString.concat(annonymousInnerSchemaName);

                final ObjectNode propertyNode = property.getPropertyNode();
                propertyNode.put(PropertyType.Id.getName(), modelSchemaUriString);

                final URI modelSchemaUri = URI.create(modelSchemaUriString);

                modelJsonSchema = jsonSchemaLoader.load(propertyNode, modelSchemaUri);
            }

            if (modelJsonSchema != jsonSchema) {

                schemaLoader.load(modelJsonSchema, baseSchemaUri);
            }

            modelValue.setModelSchemaUri(modelJsonSchema.getId());

            value = modelValue;
            break;
        }
        case List: {
            final ListValue listValue = context.newModel(ListValue.class);

            final JsonNode uniqueItemsNode = property.getValueNode(PropertyType.UniqueItems);
            if (uniqueItemsNode != null) {
                listValue.setElementUniquenessConstrained(uniqueItemsNode.asBoolean());
            }

            final JsonNode maxItemsNode = property.getValueNode(PropertyType.MaxItems);
            if (maxItemsNode != null) {
                listValue.setMaximumSize(maxItemsNode.asInt());
            }

            final JsonNode minItemsNode = property.getValueNode(PropertyType.MinItems);
            if (minItemsNode != null) {
                listValue.setMinimumSize(minItemsNode.asInt());
            }

            final JsonNode itemsNode = property.getValueNode(PropertyType.Items);
            ObjectNode itemNode = null;
            if (itemsNode instanceof ObjectNode) {
                itemNode = (ObjectNode) itemsNode;

            } else if (itemsNode instanceof ArrayNode) {

                final ArrayNode itemsArrayNode = (ArrayNode) itemsNode;
                if (itemsArrayNode.has(0)) {
                    itemNode = (ObjectNode) itemsArrayNode.get(0);
                }
            }

            if (itemNode != null) {
                Property itemsProperty;
                try {
                    itemsProperty = new Property(jsonSchema, PropertyType.Items.getName(), itemNode);
                    final Value elementValue = generateValue(jsonSchema, itemsProperty);
                    final Slot elementSlot = context.newModel(Slot.class);
                    elementSlot.setName("E");
                    elementSlot.setValue(elementValue);
                    listValue.setElementSlot(elementSlot);
                } catch (final IOException e) {
                    throw new SchemaGeneratorException(e.getMessage(), e, this);
                }
            }

            value = listValue;
            break;
        }
        case Long: {
            final LongValue longValue = context.newModel(LongValue.class);

            final JsonNode defaultNode = property.getValueNode(PropertyType.Default);
            if (defaultNode != null) {
                longValue.setDefault(defaultNode.asLong());
            }

            final JsonNode divisibleByNode = property.getValueNode(PropertyType.DivisibleBy);
            if (divisibleByNode != null) {
                longValue.setDivisibleBy(divisibleByNode.asLong());
            }

            final JsonNode multipleOfNode = property.getValueNode(PropertyType.MultipleOf);
            if (multipleOfNode != null) {
                longValue.setDivisibleBy(multipleOfNode.asLong());
            }

            final JsonNode maximumNode = property.getValueNode(PropertyType.Maximum);
            if (maximumNode != null) {
                longValue.setMaximum(maximumNode.asLong());
            }

            final JsonNode minimumNode = property.getValueNode(PropertyType.Minimum);
            if (minimumNode != null) {
                longValue.setMinimum(minimumNode.asLong());
            }

            value = longValue;
            break;
        }
        case Native: {
            break;
        }
        case SingleSelect: {
            final SingleSelectValue singleSelectValue = context.newModel(SingleSelectValue.class);
            value = singleSelectValue;
            break;
        }
        case Text: {
            final TextValue textValue = context.newModel(TextValue.class);

            final JsonNode defaultNode = property.getValueNode(PropertyType.Default);
            if (defaultNode != null) {
                textValue.setDefault(defaultNode.asText());
            }

            final JsonNode maxLengthNode = property.getValueNode(PropertyType.MaxLength);
            if (maxLengthNode != null) {
                textValue.setMaximumLength(maxLengthNode.asInt());
            }

            final JsonNode minLengthNode = property.getValueNode(PropertyType.MinLength);
            if (minLengthNode != null) {
                textValue.setMinimumLength(minLengthNode.asInt());
            }

            final JsonNode formatNode = property.getValueNode(PropertyType.Format);
            if (formatNode != null) {

                final String formatKeyword = formatNode.asText();
                final JsonStringFormat jsonStringFormat = JsonStringFormat.forKeyword(formatKeyword);
                if (jsonStringFormat != null) {
                    final Class<?> formatJavaType = jsonStringFormat.getJavaType();
                    final SyntaxLoader syntaxLoader = context.getSyntaxLoader();
                    final URI syntaxUri = syntaxLoader.getSyntaxUri(formatJavaType);
                    textValue.setSyntaxUri(syntaxUri);
                }
            }

            textValue.getDisallowedValues();

            value = textValue;

            break;
        }
        default: {
            break;
        }
        }

        if (value instanceof MaybeRequired) {
            final JsonNode requiredNode = property.getValueNode(PropertyType.Required);
            if (requiredNode != null) {
                ((MaybeRequired) value).setRequired(requiredNode.asBoolean());
            }
        }

        if (value instanceof NumericValue) {
            final NumericValue numericValue = (NumericValue) value;

            final JsonNode exclusiveMaximumNode = property.getValueNode(PropertyType.ExclusiveMaximum);
            if (exclusiveMaximumNode != null) {
                numericValue.setExclusiveMaximum(exclusiveMaximumNode.asBoolean());
            }

            final JsonNode exclusiveMinimumNode = property.getValueNode(PropertyType.ExclusiveMinimum);
            if (exclusiveMinimumNode != null) {
                numericValue.setExclusiveMinimum(exclusiveMinimumNode.asBoolean());
            }
        }

        return value;

    }

    private Value generateValue(final Type heapValueType, final ProtoSlot protoSlot) {

        final Context context = getContext();

        if (heapValueType == null) {
            return null;
        }

        final SchemaLoader schemaLoader = context.getSchemaLoader();
        final ValueType valueType = schemaLoader.getValueType(heapValueType);
        final Class<?> heapValueClass = (heapValueType instanceof Class<?>) ? (Class<?>) heapValueType : null;

        final boolean requiresNonNullValue = (heapValueClass != null && heapValueClass.isPrimitive());
        Value value = null;

        switch (valueType) {

        case Boolean:
            final BooleanValue booleanValue = context.newModel(BooleanValue.class);
            value = booleanValue;
            break;

        case Date:
            final DateValue dateValue = context.newModel(DateValue.class);
            value = dateValue;

            break;

        case Double:
            final DoubleValue doubleValue = context.newModel(DoubleValue.class);
            value = doubleValue;
            break;

        case Integer:
            final IntegerValue integerValue = context.newModel(IntegerValue.class);
            value = integerValue;
            break;

        case Link:
            final LinkValue linkValue = context.newModel(LinkValue.class);

            if (protoSlot instanceof LinkProtoSlot) {
                final LinkProtoSlot linkProtoSlot = (LinkProtoSlot) protoSlot;

                linkValue.setLinkRelationUri(linkProtoSlot.getLinkRelationUri());
                linkValue.setEmbedded(linkProtoSlot.isEmbedded());
                linkValue.setRequestSchemaUri(linkProtoSlot.getRequestSchemaUri());
                linkValue.setResponseSchemaUri(linkProtoSlot.getResponseSchemaUri());

                final List<LinkValueBinding> bindings = generateLinkValueBindingList(linkProtoSlot);
                linkValue.getBindings().addAll(bindings);
            }

            value = linkValue;
            break;

        case Model:

            final ModelValue modelValue = context.newModel(ModelValue.class);
            final URI modelSchemaUri = schemaLoader.getTypeUri(heapValueClass);
            modelValue.setModelSchemaUri(modelSchemaUri);

            value = modelValue;
            break;

        case List:

            final ListValue listValue;

            if (protoSlot instanceof CollectionPropertyProtoSlot) {
                final CollectionPropertyProtoSlot collectionPropertyProtoSlot = (CollectionPropertyProtoSlot) protoSlot;
                final CollectionValue collectionValue = context.newModel(CollectionValue.class);
                listValue = collectionValue;

                final Integer limit = collectionPropertyProtoSlot.getLimit();
                if (limit != null && limit > 0) {
                    collectionValue.setLimit(limit);
                }

                final URI linkRelationUri = collectionPropertyProtoSlot.getLinkRelationUri();
                if (linkRelationUri != null) {
                    collectionValue.setLinkRelationUri(linkRelationUri);
                }

                final ProtoSearchCriteria protoSearchCriteria = collectionPropertyProtoSlot
                        .getProtoSearchCriteria();

                final List<CollectionValueSearchCriterion> and = generateCollectionValueSearchCriterionList(
                        protoSearchCriteria.getAnd());
                collectionValue.getAnd().addAll(and);

                final List<CollectionValueSearchCriterion> or = generateCollectionValueSearchCriterionList(
                        protoSearchCriteria.getOr());
                collectionValue.getOr().addAll(or);
            } else {
                listValue = context.newModel(ListValue.class);
            }

            final Slot elementSlot = context.newModel(Slot.class);
            // TODO get this from the java class
            elementSlot.setName("E");

            final Type elementValueHeapType = ValueType.getListElementType(heapValueType);
            final Value elementValue = generateValue(elementValueHeapType, null);
            elementSlot.setValue(elementValue);
            listValue.setElementSlot(elementSlot);

            value = listValue;
            break;

        case Long:
            final LongValue longValue = context.newModel(LongValue.class);
            value = longValue;
            break;

        case Native:
            break;

        case SingleSelect:
            final SingleSelectValue singleSelectValue = context.newModel(SingleSelectValue.class);
            singleSelectValue.setChoicesUri(schemaLoader.getTypeUri(heapValueType));
            value = singleSelectValue;
            break;

        case Text:
            final TextValue textValue = context.newModel(TextValue.class);

            if (!String.class.equals(heapValueType) && heapValueClass != null) {
                final SyntaxLoader syntaxLoader = context.getSyntaxLoader();
                final URI syntaxUri = syntaxLoader.getSyntaxUri(heapValueClass);
                textValue.setSyntaxUri(syntaxUri);
            }

            value = textValue;

            break;

        default:
            break;

        }

        if (value instanceof MaybeRequired && requiresNonNullValue) {
            ((MaybeRequired) value).setRequired(requiresNonNullValue);
        }

        return value;
    }

    private String getDefaultValueString(final Value slotValue) {

        final Context context = getContext();
        final SyntaxLoader syntaxLoader = context.getSyntaxLoader();

        String defaultValueString = null;

        if (slotValue instanceof TextValue) {
            defaultValueString = ((TextValue) slotValue).getDefault();
        } else if (slotValue instanceof IntegerValue) {
            final Integer defaultValue = ((IntegerValue) slotValue).getDefault();
            if (defaultValue != null) {
                defaultValueString = String.valueOf(defaultValue.intValue());
            }
        } else if (slotValue instanceof SingleSelectValue) {
            defaultValueString = ((SingleSelectValue) slotValue).getDefault();
        } else if (slotValue instanceof MultiSelectValue) {
            final List<String> defaultValue = ((MultiSelectValue) slotValue).getDefault();
            // TODO: Revisit this format
            defaultValueString = String.valueOf(defaultValue);
        } else if (slotValue instanceof DateValue) {
            final Date defaultValue = ((DateValue) slotValue).getDefault();
            if (defaultValue != null) {
                final SyntaxHandler<Date> syntaxHandler = syntaxLoader.getSyntaxHandler(Date.class);
                defaultValueString = syntaxHandler.formatSyntaxValue(defaultValue);
            }
        } else if (slotValue instanceof BooleanValue) {
            final Boolean defaultValue = ((BooleanValue) slotValue).getDefault();
            if (defaultValue != null) {
                defaultValueString = String.valueOf(defaultValue.booleanValue());
            }
        } else if (slotValue instanceof DoubleValue) {
            final Double defaultValue = ((DoubleValue) slotValue).getDefault();
            if (defaultValue != null) {
                defaultValueString = String.valueOf(defaultValue.doubleValue());
            }
        } else if (slotValue instanceof LongValue) {
            final Long defaultValue = ((LongValue) slotValue).getDefault();
            if (defaultValue != null) {
                defaultValueString = String.valueOf(defaultValue.longValue());
            }
        }

        return defaultValueString;

    }

    private JavaBytecodeType getSlotType(final JavaBytecodeClass javaBytecodeClass, final Schema schema,
            final boolean isAggregate, final Slot slot) {

        final Context context = getContext();
        final Value slotValue = slot.getValue();

        boolean required = false;
        if (slotValue instanceof MaybeRequired) {
            final Boolean maybeRequired = ((MaybeRequired) slotValue).isRequired();
            required = (maybeRequired != null && maybeRequired);
        }

        final JavaBytecodeType type;

        if (slotValue instanceof TextValue) {

            final URI syntaxUri = ((TextValue) slotValue).getSyntaxUri();
            if (syntaxUri != null) {
                final SyntaxLoader syntaxLoader = context.getSyntaxLoader();
                final Class<?> syntaxJavaClass = syntaxLoader.getSyntaxJavaClass(syntaxUri);

                if (syntaxJavaClass != null) {
                    type = new JavaBytecodeType(syntaxJavaClass);
                } else {
                    throw new SchemaGeneratorException("Unsupported syntax, " + syntaxUri, null, this);
                }
            } else {
                type = JavaBytecodeType.StringBytecodeType;
            }
        } else if (slotValue instanceof LinkValue) {
            type = JavaBytecodeType.LinkBytecodeType;
        } else if (slotValue instanceof ModelValue) {

            final URI modelSchemaUri = ((ModelValue) slotValue).getModelSchemaUri();
            if (modelSchemaUri != null) {
                type = javaBytecodeTypeForSchemaUri(modelSchemaUri);
            } else {
                type = JavaBytecodeType.ModelBytecodeType;
            }
        } else if (slotValue instanceof ListValue) {

            final ListValue listValue = (ListValue) slotValue;
            final Slot elementSlot = listValue.getElementSlot();
            if (elementSlot != null) {
                type = new JavaBytecodeType(JavaBytecodeType.ListBytecodeType.getString());
                final JavaBytecodeType elementType = getSlotType(javaBytecodeClass, schema, isAggregate,
                        elementSlot);
                final SortedMap<String, JavaBytecodeType> parameters = new TreeMap<String, JavaBytecodeType>();
                parameters.put(elementSlot.getName(), elementType);
                type.setParameters(parameters);
            } else {
                type = JavaBytecodeType.ListBytecodeType;
            }
        } else if (slotValue instanceof BooleanValue) {

            if (required) {
                // No nulls allowed, convert from a "Boolean" to a
                // "boolean"
                type = JavaBytecodeType.BooleanPrimitiveBytecodeType;
            } else {
                type = JavaBytecodeType.BooleanBytecodeType;
            }

        } else if (slotValue instanceof IntegerValue) {

            if (required) {
                // No nulls allowed, convert from a "Integer" to a "int"
                type = JavaBytecodeType.IntegerPrimitiveBytecodeType;
            } else {
                type = JavaBytecodeType.IntegerBytecodeType;
            }
        } else if (slotValue instanceof LongValue) {

            if (required) {
                // No nulls allowed, convert from a "Long" to a "long"
                type = JavaBytecodeType.LongPrimitiveBytecodeType;
            } else {
                type = JavaBytecodeType.LongBytecodeType;
            }

        } else if (slotValue instanceof DoubleValue) {

            if (required) {
                // No nulls allowed, convert from a "Double" to a "double"
                type = JavaBytecodeType.DoublePrimitiveBytecodeType;
            } else {
                type = JavaBytecodeType.DoubleBytecodeType;
            }

        } else if (slotValue instanceof DateValue) {
            type = JavaBytecodeType.DateBytecodeType;
        } else if (slotValue instanceof NativeValue) {
            // TODO: Finish support for "extended" value types.
            type = JavaBytecodeType.ObjectBytecodeType;
        } else if (slotValue instanceof SingleSelectValue) {

            final URI choicesUri = ((SingleSelectValue) slotValue).getChoicesUri();
            if (choicesUri != null) {
                type = javaBytecodeTypeForChoicesUri(choicesUri);
            } else {
                type = JavaBytecodeType.EnumBytecodeType;
            }

        } else {
            throw new SchemaGeneratorException("Unhandled value, " + slotValue + ", found in: "
                    + schema.getUniqueName().getLocalName() + "." + slot.getName(), null, this);
        }

        return type;
    }

    private ValueType getValueType(final JsonType jsonType) {

        switch (jsonType) {
        case Any:
            return ValueType.Native;

        case Array:
            return ValueType.List;

        case Boolean:
            return ValueType.Boolean;

        case Integer:
            return ValueType.Integer;

        case Null:
            return ValueType.Native;

        case Number:
            return ValueType.Double;

        case Object:
            return ValueType.Model;

        case String:
            return ValueType.Text;

        default:
            return ValueType.Native;

        }

    }

    private JavaBytecodeType javaBytecodeTypeForChoicesUri(final URI choicesUri) {

        if (choicesUri == null) {
            return null;
        }

        return new JavaBytecodeType(uriToInternalTypeName(choicesUri));

    }

    private JavaBytecodeType javaBytecodeTypeForSchemaUri(final URI schemaUri) {

        if (schemaUri == null) {
            return null;
        }

        return new JavaBytecodeType(uriToInternalTypeName(schemaUri));

    }

    private String uriToInternalTypeName(final URI uri) {

        final SchemaLoader schemaLoader = getSchemaLoader();
        final String externalClassName = schemaLoader.getNativeTypeName(uri);
        final String internalClassName = SchemaGenerator.externalTypeNameToInternalTypeName(externalClassName);
        return internalClassName;
    }

    private void visitAnnotation(final JavaBytecodeAnnotation annotation, final ClassVisitor classVisitor,
            final MethodVisitor methodVisitor, final AnnotationVisitor parentAnnotationVisitor,
            final String parentAnnotationValueName) {

        final String descriptor = annotation.getDescriptor();

        final AnnotationVisitor annotationVisitor;
        if (parentAnnotationVisitor != null) {
            annotationVisitor = parentAnnotationVisitor.visitAnnotation(parentAnnotationValueName, descriptor);
        } else if (methodVisitor != null) {
            annotationVisitor = methodVisitor.visitAnnotation(descriptor, true);
        } else {
            annotationVisitor = classVisitor.visitAnnotation(descriptor, true);
        }

        for (final String name : annotation.getAttributeNames()) {
            final Object value = annotation.getAttributeValue(name);
            if (name != null && value != null) {
                if (value instanceof Object[]) {
                    final AnnotationVisitor arrayValueVisitor = annotationVisitor.visitArray(name);
                    final Object[] array = (Object[]) value;
                    for (final Object element : array) {
                        if (element instanceof Enum) {
                            final String enumDescriptor = new JavaBytecodeType(element.getClass()).getDescriptor();
                            final String enumValueString = ((Enum) element).name();
                            arrayValueVisitor.visitEnum(null, enumDescriptor, enumValueString);
                        } else if (element instanceof JavaBytecodeAnnotation) {
                            visitAnnotation(((JavaBytecodeAnnotation) element), classVisitor, methodVisitor,
                                    arrayValueVisitor, name);
                        } else {
                            arrayValueVisitor.visit(null, element);
                        }
                    }
                    arrayValueVisitor.visitEnd();
                } else if (value instanceof Enum) {
                    final String enumDescriptor = new JavaBytecodeType(value.getClass()).getDescriptor();
                    final String enumValueString = ((Enum) value).name();
                    annotationVisitor.visitEnum(name, enumDescriptor, enumValueString);
                } else if (value instanceof JavaBytecodeAnnotation) {
                    visitAnnotation(((JavaBytecodeAnnotation) value), classVisitor, methodVisitor,
                            annotationVisitor, name);
                } else {
                    annotationVisitor.visit(name, value);
                }
            }
        }

        annotationVisitor.visitEnd();
    }

    private static enum AnnotationParameterName {
        and, bindings, comparableSlotNames, embedded, exclusive, keySlotNames, limit, linkRelationUri, method, operator, or, referenceSlot, regex, titleSlotName, uniqueName, value, valueSource, valueSourceType;
    }

}