com.ctriposs.rest4j.internal.server.model.ResourceModelEncoder.java Source code

Java tutorial

Introduction

Here is the source code for com.ctriposs.rest4j.internal.server.model.ResourceModelEncoder.java

Source

/**
 * Copyright (C) 2014 the original author or authors.
 * See the notice.md file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 com.ctriposs.rest4j.internal.server.model;

import com.ctriposs.rest4j.internal.server.Rest4JInternalException;
import com.ctriposs.data.DataMap;
import com.ctriposs.data.codec.DataCodec;
import com.ctriposs.data.codec.JacksonDataCodec;
import com.ctriposs.data.schema.DataSchema;
import com.ctriposs.data.schema.JsonBuilder;
import com.ctriposs.data.schema.NamedDataSchema;
import com.ctriposs.data.schema.PrimitiveDataSchema;
import com.ctriposs.data.schema.SchemaToJsonEncoder;
import com.ctriposs.data.schema.TyperefDataSchema;
import com.ctriposs.data.schema.UnionDataSchema;
import com.ctriposs.data.template.DataTemplate;
import com.ctriposs.data.template.DataTemplateUtil;
import com.ctriposs.data.template.HasTyperefInfo;
import com.ctriposs.data.template.StringArray;
import com.ctriposs.data.template.TyperefInfo;
import com.ctriposs.rest4j.common.ActionResponse;
import com.ctriposs.rest4j.common.ComplexResourceKey;
import com.ctriposs.rest4j.common.ResourceMethod;
import com.ctriposs.rest4j.common.RestConstants;
import com.ctriposs.rest4j.restspec.ActionSchema;
import com.ctriposs.rest4j.restspec.ActionSchemaArray;
import com.ctriposs.rest4j.restspec.ActionsSetSchema;
import com.ctriposs.rest4j.restspec.AssocKeySchema;
import com.ctriposs.rest4j.restspec.AssocKeySchemaArray;
import com.ctriposs.rest4j.restspec.AssociationSchema;
import com.ctriposs.rest4j.restspec.CollectionSchema;
import com.ctriposs.rest4j.restspec.CustomAnnotationContentSchemaMap;
import com.ctriposs.rest4j.restspec.EntitySchema;
import com.ctriposs.rest4j.restspec.FinderSchema;
import com.ctriposs.rest4j.restspec.FinderSchemaArray;
import com.ctriposs.rest4j.restspec.IdentifierSchema;
import com.ctriposs.rest4j.restspec.MetadataSchema;
import com.ctriposs.rest4j.restspec.ParameterSchema;
import com.ctriposs.rest4j.restspec.ParameterSchemaArray;
import com.ctriposs.rest4j.restspec.ResourceSchema;
import com.ctriposs.rest4j.restspec.ResourceSchemaArray;
import com.ctriposs.rest4j.restspec.RestMethodSchema;
import com.ctriposs.rest4j.restspec.RestMethodSchemaArray;
import com.ctriposs.rest4j.restspec.SimpleSchema;
import com.ctriposs.rest4j.server.Key;
import com.ctriposs.rest4j.server.ResourceLevel;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import org.apache.commons.io.IOUtils;

/**
 * Encodes a ResourceModel (runtime-reflection oriented class) into the JSON-serializable
 * {@link ResourceSchema}. Accepts a {@link DocsProvider} plugin to incorporate documentation
 * from a JVM language such as JavaDoc and Scaladoc.
 *
 */
public class ResourceModelEncoder {
    public static final String DEPRECATED_ANNOTATION_NAME = "deprecated";
    public static final String DEPRECATED_ANNOTATION_DOC_FIELD = "doc";
    public static final String COMPOUND_KEY_TYPE_NAME = "CompoundKey";

    private final DataCodec codec = new JacksonDataCodec();

    /**
     * Provides documentation strings from a JVM language to be incorporated into ResourceModels.
     */
    public interface DocsProvider {
        /**
         * Gets the file extensions this doc provider supports.  It must ignore any source files registered with it that
         * are not in this set.
         *
         * @return the supported files extensions
         */
        Set<String> supportedFileExtensions();

        /**
         * Registers the source files with the doc provider.  The doc provider should perform any initialization required
         * handle subsequent requests for classe, method and param docs from these source files.
         *
         * @param filenames provides the source file names to register.
         */
        void registerSourceFiles(Collection<String> filenames);

        /**
         * @param resourceClass resource class
         * @return class documentation, or null if no documentation is available
         */
        String getClassDoc(Class<?> resourceClass);

        String getClassDeprecatedTag(Class<?> resourceClass);

        /**
         * @param method resource {@link Method}
         * @return method documentation, or null if no documentation is available
         */
        String getMethodDoc(Method method);

        String getMethodDeprecatedTag(Method method);

        /**
         * @param method resource {@link Method}
         * @param name method param name
         * @return method param documentation, or null if no documentation is available
         */
        String getParamDoc(Method method, String name);
    }

    public static class NullDocsProvider implements DocsProvider {
        @Override
        public void registerSourceFiles(Collection<String> filenames) {

        }

        @Override
        public Set<String> supportedFileExtensions() {
            return Collections.emptySet();
        }

        @Override
        public String getClassDoc(final Class<?> resourceClass) {
            return null;
        }

        @Override
        public String getClassDeprecatedTag(Class<?> resourceClass) {
            return null;
        }

        @Override
        public String getMethodDoc(final Method method) {
            return null;
        }

        @Override
        public String getMethodDeprecatedTag(Method method) {
            return null;
        }

        @Override
        public String getParamDoc(final Method method, final String name) {
            return null;
        }
    }

    private final DocsProvider _docsProvider;

    /**
     * @param docsProvider {@link DocsProvider} to pull in javadoc comments.
     */
    public ResourceModelEncoder(final DocsProvider docsProvider) {
        _docsProvider = docsProvider;
    }

    /**
     * @param resourceModel {@link ResourceModel} to build the schema for
     * @return {@link ResourceSchema} for the provided resource model
     */
    public ResourceSchema buildResourceSchema(final ResourceModel resourceModel) {
        ResourceSchema rootNode = new ResourceSchema();

        switch (resourceModel.getResourceType()) {
        case ACTIONS:
            appendActionsModel(rootNode, resourceModel);
            break;
        case SIMPLE:
            appendSimple(rootNode, resourceModel);
            break;
        default:
            appendCollection(rootNode, resourceModel);
            break;
        }

        final DataMap customAnnotation = resourceModel.getCustomAnnotationData();
        if (!customAnnotation.isEmpty()) {
            rootNode.setAnnotations(new CustomAnnotationContentSchemaMap(customAnnotation));
        }

        return rootNode;
    }

    /**
     * Checks if a matching .restspec.json file exists in the classpath for the given {@link ResourceModel}.
     * If one is found it is loaded.  If not, one is built from the {@link ResourceModel}.
     *
     * The .restspec.json is preferred because it contains the exact idl that was generated for the resource
     * and also includees includes javadoc from the server class in the restspec.json.
     *
     * @param resourceModel provides the name and namespace of the schema to load or build
     * @return the {@link ResourceSchema} for the given {@link ResourceModel}
     */
    public ResourceSchema loadOrBuildResourceSchema(final ResourceModel resourceModel) {
        StringBuilder resourceFilePath = new StringBuilder(File.separator);
        if (resourceModel.getNamespace() != null) {
            resourceFilePath.append(resourceModel.getNamespace());
            resourceFilePath.append(".");
        }
        resourceFilePath.append(resourceModel.getName());
        resourceFilePath.append(RestConstants.RESOURCE_MODEL_FILENAME_EXTENSION);

        try {
            InputStream stream = this.getClass().getClassLoader().getResourceAsStream(resourceFilePath.toString());
            if (stream == null) {
                // restspec.json file not found, building one instead
                return buildResourceSchema(resourceModel);
            } else {
                DataMap resourceSchemaDataMap = codec.bytesToMap(IOUtils.toByteArray(stream));
                return new ResourceSchema(resourceSchemaDataMap);
            }
        } catch (IOException e) {
            throw new RuntimeException("Failed to read " + resourceFilePath.toString() + " from classpath.", e);
        }
    }

    /*package*/ static String buildDataSchemaType(final Class<?> type) {
        final DataSchema schema = DataTemplateUtil.getSchema(type);
        return buildDataSchemaType(schema);
    }

    /*package*/ static String buildDataSchemaType(DataSchema schema) {
        if (schema instanceof PrimitiveDataSchema || schema instanceof NamedDataSchema) {
            return schema.getUnionMemberKey();
        }

        JsonBuilder builder = null;
        try {
            builder = new JsonBuilder(JsonBuilder.Pretty.SPACES);
            final SchemaToJsonEncoder encoder = new NamedSchemaReferencingJsonEncoder(builder);
            encoder.encode(schema);
            return builder.result();
        } catch (IOException e) {
            throw new Rest4JInternalException("could not encode schema for '" + schema.toString() + "'", e);
        } finally {
            if (builder != null) {
                builder.closeQuietly();
            }
        }
    }

    /**
     * SchemaToJsonEncoder which encodes all NamedDataSchemas as name references.  This encoder
     * never inlines the full schema text of a NamedDataSchema.
     */
    private static class NamedSchemaReferencingJsonEncoder extends SchemaToJsonEncoder {
        public NamedSchemaReferencingJsonEncoder(final JsonBuilder builder) {
            super(builder);
        }

        @Override
        protected void encodeNamed(final NamedDataSchema schema) throws IOException {
            writeSchemaName(schema);
            return;
        }
    }

    private static String buildDataSchemaType(final Class<?> type, final DataSchema dataSchema) {
        final DataSchema schemaToEncode;
        if (dataSchema instanceof TyperefDataSchema) {
            return ((TyperefDataSchema) dataSchema).getFullName();
        } else if (dataSchema instanceof PrimitiveDataSchema || dataSchema instanceof NamedDataSchema) {
            return dataSchema.getUnionMemberKey();
        } else if (dataSchema instanceof UnionDataSchema && HasTyperefInfo.class.isAssignableFrom(type)) {
            final TyperefInfo unionRef = DataTemplateUtil.getTyperefInfo(type.asSubclass(DataTemplate.class));
            schemaToEncode = unionRef.getSchema();
        } else {
            schemaToEncode = dataSchema;
        }

        JsonBuilder builder = null;
        try {
            builder = new JsonBuilder(JsonBuilder.Pretty.SPACES);
            final SchemaToJsonEncoder encoder = new NamedSchemaReferencingJsonEncoder(builder);
            encoder.encode(schemaToEncode);
            return builder.result();
        } catch (IOException e) {
            throw new Rest4JInternalException("could not encode schema for '" + type.getName() + "'", e);
        } finally {
            if (builder != null) {
                builder.closeQuietly();
            }
        }
    }

    public static String buildPath(final ResourceModel resourceModel) {
        StringBuilder sb = new StringBuilder();
        buildPathInternal(resourceModel, sb, false);
        return sb.toString();
    }

    private static String buildPathForEntity(final ResourceModel resourceModel) {
        StringBuilder sb = new StringBuilder();
        buildPathInternal(resourceModel, sb, true);
        return sb.toString();
    }

    private static void buildPathInternal(ResourceModel resourceModel, final StringBuilder sb,
            boolean addEntityElement) {
        do {
            if (addEntityElement) {
                if (resourceModel.getKeys().size() >= 1) {
                    sb.insert(0, "/{" + resourceModel.getKeyName() + "}");
                }
            }
            sb.insert(0, "/" + resourceModel.getName());
            addEntityElement = true;
        } while ((resourceModel = resourceModel.getParentResourceModel()) != null);
    }

    private void appendCommon(final ResourceModel resourceModel, final ResourceSchema resourceSchema) {
        resourceSchema.setName(resourceModel.getName());
        if (!resourceModel.getNamespace().isEmpty()) {
            resourceSchema.setNamespace(resourceModel.getNamespace());
        }
        resourceSchema.setPath(buildPath(resourceModel));

        final Class<?> valueClass = resourceModel.getValueClass();
        if (valueClass != null) {
            resourceSchema.setSchema(DataTemplateUtil.getSchema(valueClass).getUnionMemberKey());
        }

        final Class<?> resourceClass = resourceModel.getResourceClass();
        final String doc = _docsProvider.getClassDoc(resourceClass);
        final StringBuilder docBuilder = new StringBuilder();
        if (doc != null) {
            docBuilder.append(doc).append("\n\n");
        }
        docBuilder.append("generated from: ").append(resourceClass.getCanonicalName());

        final String deprecatedDoc = _docsProvider.getClassDeprecatedTag(resourceClass);
        if (deprecatedDoc != null) {
            DataMap customAnnotationData = resourceModel.getCustomAnnotationData();
            if (customAnnotationData == null) {
                customAnnotationData = new DataMap();
                resourceModel.setCustomAnnotation(customAnnotationData);
            }
            customAnnotationData.put(DEPRECATED_ANNOTATION_NAME, deprecateDocToAnnotationMap(deprecatedDoc));
        }

        resourceSchema.setDoc(docBuilder.toString());
    }

    private void appendCollection(final ResourceSchema resourceSchema, final ResourceModel collectionModel) {
        appendCommon(collectionModel, resourceSchema);

        CollectionSchema collectionSchema = new CollectionSchema();
        //HACK: AssociationSchema and CollectionSchema share many common elements, but have no inheritance
        //relationship.  Here, we construct them both as facades on the same DataMap, which allows
        //us to pass strongly typed CollectionSchema objects around, even when we're dealing with
        //an association.
        AssociationSchema associationSchema = new AssociationSchema(collectionSchema.data());

        if (collectionModel.getKeys().size() == 1) {
            appendIdentifierNode(collectionSchema, collectionModel);
        } else {
            appendKeys(associationSchema, collectionModel);
        }

        appendSupportsNodeToCollectionSchema(collectionSchema, collectionModel);
        appendMethodsToCollectionSchema(collectionSchema, collectionModel);
        FinderSchemaArray finders = createFinders(collectionModel);
        if (finders.size() > 0) {
            collectionSchema.setFinders(finders);
        }
        ActionSchemaArray actions = createActions(collectionModel, ResourceLevel.COLLECTION);
        if (actions.size() > 0) {
            collectionSchema.setActions(actions);
        }
        appendEntityToCollectionSchema(collectionSchema, collectionModel);

        switch (collectionModel.getResourceType()) {
        case COLLECTION:
            resourceSchema.setCollection(collectionSchema);
            break;
        case ASSOCIATION:
            resourceSchema.setAssociation(associationSchema);
            break;
        default:
            throw new IllegalArgumentException("unsupported resource type");
        }
    }

    private void appendActionsModel(final ResourceSchema resourceSchema, final ResourceModel resourceModel) {
        appendCommon(resourceModel, resourceSchema);
        ActionsSetSchema actionsNode = new ActionsSetSchema();
        ActionSchemaArray actions = createActions(resourceModel, ResourceLevel.COLLECTION);
        if (actions.size() > 0) {
            actionsNode.setActions(actions);
        }
        resourceSchema.setActionsSet(actionsNode);
    }

    private void appendSimple(ResourceSchema resourceSchema, ResourceModel resourceModel) {
        appendCommon(resourceModel, resourceSchema);

        SimpleSchema simpleSchema = new SimpleSchema();

        appendSupportsNodeToSimpleSchema(simpleSchema, resourceModel);
        appendMethodsToSimpleSchema(simpleSchema, resourceModel);

        ActionSchemaArray actions = createActions(resourceModel, ResourceLevel.ENTITY);
        if (actions.size() > 0) {
            simpleSchema.setActions(actions);
        }

        appendEntityToSimpleSchema(simpleSchema, resourceModel);

        resourceSchema.setSimple(simpleSchema);
    }

    private void appendEntityToCollectionSchema(final CollectionSchema collectionSchema,
            final ResourceModel resourceModel) {
        EntitySchema entityNode = buildEntitySchema(resourceModel);

        collectionSchema.setEntity(entityNode);
    }

    private void appendEntityToSimpleSchema(final SimpleSchema simpleSchema, final ResourceModel resourceModel) {
        EntitySchema entityNode = buildEntitySchema(resourceModel);

        simpleSchema.setEntity(entityNode);
    }

    private EntitySchema buildEntitySchema(ResourceModel resourceModel) {
        EntitySchema entityNode = new EntitySchema();
        entityNode.setPath(buildPathForEntity(resourceModel));

        if (resourceModel.getResourceLevel() == ResourceLevel.COLLECTION) {
            ActionSchemaArray actions = createActions(resourceModel, ResourceLevel.ENTITY);
            if (actions.size() > 0) {
                entityNode.setActions(actions);
            }
        }

        // subresources
        ResourceSchemaArray subresources = new ResourceSchemaArray();
        for (ResourceModel subResourceModel : resourceModel.getSubResources()) {
            ResourceSchema subresource = new ResourceSchema();

            switch (subResourceModel.getResourceType()) {
            case COLLECTION:
            case ASSOCIATION:
                appendCollection(subresource, subResourceModel);
                break;
            case SIMPLE:
                appendSimple(subresource, subResourceModel);
                break;
            default:
                break;
            }

            final DataMap customAnnotation = subResourceModel.getCustomAnnotationData();
            if (!customAnnotation.isEmpty()) {
                subresource.setAnnotations(new CustomAnnotationContentSchemaMap(customAnnotation));
            }

            subresources.add(subresource);
        }

        if (subresources.size() > 0) {
            Collections.sort(subresources, new Comparator<ResourceSchema>() {
                @Override
                public int compare(ResourceSchema resourceSchema, ResourceSchema resourceSchema2) {
                    return resourceSchema.getName().compareTo(resourceSchema2.getName());
                }
            });
            entityNode.setSubresources(subresources);
        }
        return entityNode;
    }

    private void appendKeys(final AssociationSchema associationSchema, final ResourceModel collectionModel) {
        AssocKeySchemaArray assocKeySchemaArray = new AssocKeySchemaArray();
        List<Key> sortedKeys = new ArrayList<Key>(collectionModel.getKeys());
        Collections.sort(sortedKeys, new Comparator<Key>() {
            @Override
            public int compare(final Key o1, final Key o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });

        for (Key key : sortedKeys) {
            AssocKeySchema assocKeySchema = new AssocKeySchema();
            assocKeySchema.setName(key.getName());
            assocKeySchema.setType(buildDataSchemaType(key.getType(), key.getDataSchema()));
            assocKeySchemaArray.add(assocKeySchema);
        }

        associationSchema.setAssocKeys(assocKeySchemaArray);

        associationSchema.setIdentifier(collectionModel.getKeyName());

    }

    @SuppressWarnings("unchecked")
    private ActionSchemaArray createActions(final ResourceModel resourceModel, final ResourceLevel resourceLevel) {
        ActionSchemaArray actionsArray = new ActionSchemaArray();

        List<ResourceMethodDescriptor> resourceMethodDescriptors = resourceModel.getResourceMethodDescriptors();
        Collections.sort(resourceMethodDescriptors, new Comparator<ResourceMethodDescriptor>() {
            @Override
            public int compare(final ResourceMethodDescriptor o1, final ResourceMethodDescriptor o2) {
                if (o1.getType().equals(ResourceMethod.ACTION)) {
                    if (o2.getType().equals(ResourceMethod.ACTION)) {
                        return o1.getActionName().compareTo(o2.getActionName());
                    } else {
                        return 1;
                    }
                } else if (o2.getType().equals(ResourceMethod.ACTION)) {
                    return -1;
                } else {
                    return 0;
                }
            }
        });

        for (ResourceMethodDescriptor resourceMethodDescriptor : resourceMethodDescriptors) {
            if (ResourceMethod.ACTION.equals(resourceMethodDescriptor.getType())) {
                //do not apply entity-level actions at collection level or vice-versa
                if (resourceMethodDescriptor.getActionResourceLevel() != resourceLevel) {
                    continue;
                }

                ActionSchema action = new ActionSchema();

                action.setName(resourceMethodDescriptor.getActionName());

                String doc = _docsProvider.getMethodDoc(resourceMethodDescriptor.getMethod());
                if (doc != null) {
                    action.setDoc(doc);
                }

                ParameterSchemaArray parameters = createParameters(resourceMethodDescriptor);
                if (parameters.size() > 0) {
                    action.setParameters(parameters);
                }

                Class<?> returnType = resourceMethodDescriptor.getActionReturnType();
                if (returnType != Void.TYPE) {
                    String returnTypeString = buildDataSchemaType(returnType, resourceMethodDescriptor
                            .getActionReturnRecordDataSchema().getField(ActionResponse.VALUE_NAME).getType());
                    action.setReturns(returnTypeString);
                }

                final DataMap customAnnotation = resourceMethodDescriptor.getCustomAnnotationData();
                String deprecatedDoc = _docsProvider.getMethodDeprecatedTag(resourceMethodDescriptor.getMethod());
                if (deprecatedDoc != null) {
                    customAnnotation.put(DEPRECATED_ANNOTATION_NAME, deprecateDocToAnnotationMap(deprecatedDoc));
                }

                if (!customAnnotation.isEmpty()) {
                    action.setAnnotations(new CustomAnnotationContentSchemaMap(customAnnotation));
                }

                actionsArray.add(action);
            }
        }
        return actionsArray;
    }

    private DataMap deprecateDocToAnnotationMap(String deprecatedDoc) {
        deprecatedDoc = deprecatedDoc.trim();
        DataMap deprecatedAnnotation = new DataMap();
        if (!deprecatedDoc.isEmpty()) {
            deprecatedAnnotation.put(DEPRECATED_ANNOTATION_DOC_FIELD, deprecatedDoc);
        }
        return deprecatedAnnotation;
    }

    private FinderSchemaArray createFinders(final ResourceModel resourceModel) {
        FinderSchemaArray findersArray = new FinderSchemaArray();

        List<ResourceMethodDescriptor> resourceMethodDescriptors = resourceModel.getResourceMethodDescriptors();
        Collections.sort(resourceMethodDescriptors, new Comparator<ResourceMethodDescriptor>() {
            @Override
            public int compare(final ResourceMethodDescriptor o1, final ResourceMethodDescriptor o2) {
                if (o1.getFinderName() == null) {
                    return -1;
                } else if (o2.getFinderName() == null) {
                    return 1;
                }

                return o1.getFinderName().compareTo(o2.getFinderName());
            }
        });

        for (ResourceMethodDescriptor resourceMethodDescriptor : resourceMethodDescriptors) {
            if (ResourceMethod.FINDER.equals(resourceMethodDescriptor.getType())) {
                FinderSchema finder = new FinderSchema();

                finder.setName(resourceMethodDescriptor.getFinderName());

                String doc = _docsProvider.getMethodDoc(resourceMethodDescriptor.getMethod());
                if (doc != null) {
                    finder.setDoc(doc);
                }

                ParameterSchemaArray parameters = createParameters(resourceMethodDescriptor);
                if (parameters.size() > 0) {
                    finder.setParameters(parameters);
                }
                StringArray assocKeys = createAssocKeyParameters(resourceMethodDescriptor);
                if (assocKeys.size() > 0) {
                    finder.setAssocKeys(assocKeys);
                }
                if (resourceMethodDescriptor.getFinderMetadataType() != null) {
                    Class<?> metadataType = resourceMethodDescriptor.getFinderMetadataType();
                    MetadataSchema metadataSchema = new MetadataSchema();
                    metadataSchema.setType(buildDataSchemaType(metadataType));
                    finder.setMetadata(metadataSchema);
                }

                final DataMap customAnnotation = resourceMethodDescriptor.getCustomAnnotationData();

                String deprecatedDoc = _docsProvider.getMethodDeprecatedTag(resourceMethodDescriptor.getMethod());
                if (deprecatedDoc != null) {
                    customAnnotation.put(DEPRECATED_ANNOTATION_NAME, deprecateDocToAnnotationMap(deprecatedDoc));
                }

                if (!customAnnotation.isEmpty()) {
                    finder.setAnnotations(new CustomAnnotationContentSchemaMap(customAnnotation));
                }

                findersArray.add(finder);
            }
        }
        return findersArray;
    }

    private StringArray createAssocKeyParameters(final ResourceMethodDescriptor resourceMethodDescriptor) {
        StringArray assocKeys = new StringArray();
        for (Parameter<?> param : resourceMethodDescriptor.getParameters()) {
            // assocKeys are listed outside the parameters list
            if (param.getParamType() == Parameter.ParamType.KEY) {
                assocKeys.add(param.getName());
                continue;
            }
        }
        return assocKeys;
    }

    private ParameterSchemaArray createParameters(final ResourceMethodDescriptor resourceMethodDescriptor) {
        ParameterSchemaArray parameterSchemaArray = new ParameterSchemaArray();
        for (Parameter<?> param : resourceMethodDescriptor.getParameters()) {
            //only custom parameters need to be specified in the IDL
            if (!param.isCustom()) {
                continue;
            }

            // assocKeys are listed outside the parameters list
            if (param.getParamType() == Parameter.ParamType.KEY) {
                continue;
            }

            ParameterSchema paramSchema = new ParameterSchema();
            paramSchema.setName(param.getName());
            paramSchema.setType(buildDataSchemaType(param.getType(), param.getDataSchema()));

            final Object defaultValueData = param.getDefaultValueData();
            if (defaultValueData == null && param.isOptional()) {
                paramSchema.setOptional(true);
            } else if (defaultValueData != null) {
                paramSchema.setDefault(defaultValueData.toString());
            }

            String paramDoc = _docsProvider.getParamDoc(resourceMethodDescriptor.getMethod(), param.getName());
            if (paramDoc != null) {
                paramSchema.setDoc(paramDoc);
            }

            final DataMap customAnnotation = param.getCustomAnnotationData();
            if (!customAnnotation.isEmpty()) {
                paramSchema.setAnnotations(new CustomAnnotationContentSchemaMap(customAnnotation));
            }

            parameterSchemaArray.add(paramSchema);
        }

        return parameterSchemaArray;
    }

    private RestMethodSchemaArray createRestMethods(final ResourceModel resourceModel) {
        RestMethodSchemaArray restMethods = new RestMethodSchemaArray();

        ResourceMethod[] crudMethods = { ResourceMethod.CREATE, ResourceMethod.GET, ResourceMethod.UPDATE,
                ResourceMethod.PARTIAL_UPDATE, ResourceMethod.DELETE, ResourceMethod.BATCH_CREATE,
                ResourceMethod.BATCH_GET, ResourceMethod.BATCH_UPDATE, ResourceMethod.BATCH_PARTIAL_UPDATE,
                ResourceMethod.BATCH_DELETE, ResourceMethod.GET_ALL };

        for (ResourceMethod method : crudMethods) {
            ResourceMethodDescriptor descriptor = resourceModel.findMethod(method);
            if (descriptor == null) {
                continue;
            }

            RestMethodSchema restMethod = new RestMethodSchema();

            restMethod.setMethod(method.toString());

            String doc = _docsProvider.getMethodDoc(descriptor.getMethod());
            if (doc != null) {
                restMethod.setDoc(doc);
            }

            ParameterSchemaArray parameters = createParameters(descriptor);
            if (parameters.size() > 0) {
                restMethod.setParameters(parameters);
            }

            final DataMap customAnnotation = descriptor.getCustomAnnotationData();

            String deprecatedDoc = _docsProvider.getMethodDeprecatedTag(descriptor.getMethod());
            if (deprecatedDoc != null) {
                customAnnotation.put(DEPRECATED_ANNOTATION_NAME, deprecateDocToAnnotationMap(deprecatedDoc));
            }

            if (!customAnnotation.isEmpty()) {
                restMethod.setAnnotations(new CustomAnnotationContentSchemaMap(customAnnotation));
            }

            restMethods.add(restMethod);
        }

        return restMethods;
    }

    private void appendSupportsNodeToCollectionSchema(final CollectionSchema collectionSchema,
            final ResourceModel resourceModel) {
        StringArray supportsArray = buildSupportsNode(resourceModel);
        collectionSchema.setSupports(supportsArray);
    }

    private void appendMethodsToCollectionSchema(CollectionSchema collectionSchema, ResourceModel resourceModel) {
        RestMethodSchemaArray restMethods = createRestMethods(resourceModel);
        if (restMethods.size() > 0) {
            collectionSchema.setMethods(restMethods);
        }
    }

    private void appendSupportsNodeToSimpleSchema(final SimpleSchema simpleSchema,
            final ResourceModel resourceModel) {
        StringArray supportsArray = buildSupportsNode(resourceModel);
        simpleSchema.setSupports(supportsArray);
    }

    private void appendMethodsToSimpleSchema(SimpleSchema simpleSchema, ResourceModel resourceModel) {
        RestMethodSchemaArray restMethods = createRestMethods(resourceModel);
        if (restMethods.size() > 0) {
            simpleSchema.setMethods(restMethods);
        }
    }

    private StringArray buildSupportsNode(ResourceModel resourceModel) {
        StringArray supportsArray = new StringArray();

        buildSupportsArray(resourceModel, supportsArray);
        return supportsArray;
    }

    private void buildSupportsArray(final ResourceModel resourceModel, final StringArray supportsArray) {
        List<String> supportsStrings = new ArrayList<String>();
        for (ResourceMethodDescriptor resourceMethodDescriptor : resourceModel.getResourceMethodDescriptors()) {
            ResourceMethod type = resourceMethodDescriptor.getType();
            if (!type.equals(ResourceMethod.FINDER) && !type.equals(ResourceMethod.ACTION)) {
                supportsStrings.add(type.toString());
            }
        }

        Collections.sort(supportsStrings);

        for (String s : supportsStrings) {
            supportsArray.add(s);
        }
    }

    private void appendIdentifierNode(final CollectionSchema collectionNode,
            final ResourceModel collectionResource) {
        IdentifierSchema identifierSchema = new IdentifierSchema();
        identifierSchema.setName(collectionResource.getKeyName());
        // If the key is a complex key, set type to the schema type of the key part of the
        // complex key and params to that of the params part of the complex key.
        // Otherwise, just set the type to the key class schema type
        if (collectionResource.getKeyClass().equals(ComplexResourceKey.class)) {
            identifierSchema.setType(buildDataSchemaType(collectionResource.getKeyKeyClass()));
            identifierSchema.setParams(buildDataSchemaType(collectionResource.getKeyParamsClass()));
        } else {
            Key key = collectionResource.getPrimaryKey();
            identifierSchema.setType(buildDataSchemaType(key.getType(), key.getDataSchema()));
        }

        collectionNode.setIdentifier(identifierSchema);
    }
}