ApiDescription.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 */
package org.forgerock.api.models;

import static org.forgerock.api.models.Definitions.definitions;
import static org.forgerock.api.models.Errors.errors;
import static org.forgerock.api.models.Paths.paths;
import static org.forgerock.api.models.Services.services;
import static org.forgerock.api.util.ValidationUtil.isEmpty;

import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.forgerock.api.ApiValidationException;
import org.forgerock.util.i18n.LocalizableString;

/** Class that represents the ApiDescription type in API descriptor. */
@JsonDeserialize(builder = ApiDescription.Builder.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class ApiDescription {

    private final String id;
    private final String version;
    private final LocalizableString description;
    private final Definitions definitions;
    private final Services services;
    private final Errors errors;
    private final Paths paths;

    private ApiDescription(Builder builder) {
        this.id = builder.id;
        this.version = builder.version;
        this.description = builder.description;
        this.definitions = builder.definitions == null ? definitions().build() : builder.definitions;
        this.services = builder.services == null ? services().build() : builder.services;
        this.errors = builder.errors == null ? errors().build() : builder.errors;
        this.paths = builder.paths == null ? paths().build() : builder.paths;

        if (isEmpty(id) || isEmpty(version)) {
            throw new ApiValidationException("id and version required");
        }
    }

    /**
     * Getter of id.
     *
     * @return id
     */
    public String getId() {
        return id;
    }

    /**
     * Getter of version.
     *
     * @return The version.
     */
    public String getVersion() {
        return version;
    }

    /**
     * Gets description of API Descriptor.
     *
     * @return Description of API Descriptor
     */
    public LocalizableString getDescription() {
        return description;
    }

    /**
     * Getter of definitions.
     *
     * @return Definitions map
     */
    public Definitions getDefinitions() {
        return definitions.getDefinitions().isEmpty() ? null : definitions;
    }

    /**
     * This allows the models package to mutate the schema defined here. This is used when processing
     * annotations on resources that may reference schemas using an id, so those schemas need to be defined here
     * rather than in-line in the resource descriptions.
     *
     * @param id The definition id.
     * @param schema The definition.
     */
    void addDefinition(String id, Schema schema) {
        if (schema.getReference() != null) {
            throw new IllegalArgumentException("Cannot define a schema using a reference");
        }
        Schema defined = definitions.get(id);
        if (defined != null && !defined.equals(schema)) {
            throw new IllegalArgumentException("Trying to redefine already defined schema, " + id);
        }
        definitions.getDefinitions().put(id, schema);
    }

    /**
     * Getter of services.
     *
     * @return Services map
     */
    public Services getServices() {
        return services.getServices().isEmpty() ? null : services;
    }

    /**
     * This allows the models package to mutate the resources defined here. This is used when processing
     * annotations on resources that may reference resources using an id, so those services need to be defined here
     * rather than in-line in the resource descriptions.
     *
     * @param id The resource id.
     * @param resource The resource definition.
     * @see Resource#fromAnnotatedType(Class, Resource.AnnotatedTypeVariant, ApiDescription)
     */
    void addService(String id, Resource resource) {
        if (resource.getReference() != null) {
            throw new IllegalArgumentException("Cannot define a resource using a reference");
        }
        Resource defined = services.get(id);
        if (defined != null && !defined.equals(resource)) {
            throw new IllegalArgumentException("Trying to redefine already defined resource, " + id);
        }
        services.getServices().put(id, resource);
    }

    /**
     * Getter of errors.
     *
     * @return Errors map
     */
    public Errors getErrors() {
        return errors.getErrors().isEmpty() ? null : errors;
    }

    /**
     * This allows the models package to mutate the errors defined here. This is used when processing annotations on
     * resources that may reference errors using an id, so those errors need to be defined here rather than in-line in
     * the resource descriptions.
     *
     * @param id The error id.
     * @param apiError The error definition.
     * @see ApiError#fromAnnotation(org.forgerock.api.annotations.ApiError, ApiDescription, Class)
     */
    void addError(String id, ApiError apiError) {
        if (apiError.getReference() != null) {
            throw new IllegalArgumentException("Cannot define an apiError using a reference");
        }
        ApiError defined = errors.get(id);
        if (defined != null && !defined.equals(apiError)) {
            throw new IllegalArgumentException("Trying to redefine already defined apiError, " + id);
        }
        errors.getErrors().put(id, apiError);
    }

    /**
     * Getter of paths.
     *
     * @return Paths
     */
    // Jackson queries PathsModule.PathsSerializer.isEmpty() to know whether a Paths object is empty
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    public Paths getPaths() {
        return paths;
    }

    /**
     * Create a new Builder for ApiDescription.
     *
     * @return Builder
     */
    public static Builder apiDescription() {
        return new Builder();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ApiDescription that = (ApiDescription) o;
        return Objects.equals(id, that.id)
                && Objects.equals(version, that.version)
                && Objects.equals(description, that.description)
                && Objects.equals(definitions, that.definitions)
                && Objects.equals(services, that.services)
                && Objects.equals(errors, that.errors)
                && Objects.equals(paths, that.paths);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, version, description, definitions, services, errors, paths);
    }

    /**
     * Builder for the ApiDescription.
     */
    public static final class Builder {

        private String id;
        private LocalizableString description;
        private Definitions definitions;
        private Errors errors;
        private Services services;
        private Paths paths;
        private String version;

        /**
         * Private default constructor with the mandatory fields.
         */
        private Builder() {
        }

        /**
         * Set the id.
         *
         * @param id ApiDescription id
         * @return Builder
         */
        @JsonProperty("id")
        public Builder id(String id) {
            this.id = id;
            return this;
        }

        /**
         * Sets the description.
         *
         * @param description Description of API Description
         * @return Builder
         */
        @JsonProperty("description")
        public Builder description(String description) {
            this.description = new LocalizableString(description);
            return this;
        }

        /**
         * Sets the description.
         *
         * @param description Description of API Description
         * @return Builder
         */
        public Builder description(LocalizableString description) {
            this.description = description;
            return this;
        }

        /**
         * Set the definitions.
         *
         * @param definitions Definitions for this API Description
         * @return Builder
         */
        @JsonProperty("definitions")
        public Builder definitions(Definitions definitions) {
            this.definitions = definitions;
            return this;
        }


        /**
         * Set the services.
         *
         * @param services Services for this API Description
         * @return Builder
         */
        @JsonProperty("services")
        public Builder services(Services services) {
            this.services = services;
            return this;
        }

        /**
         * Set the errors.
         *
         * @param errors Errors for this API Description
         * @return Builder
         */
        @JsonProperty("errors")
        public Builder errors(Errors errors) {
            this.errors = errors;
            return this;
        }

        /**
         * Set the paths.
         *
         * @param paths Paths
         * @return Builder
         */
        @JsonProperty("paths")
        public Builder paths(Paths paths) {
            this.paths = paths;
            return this;
        }

        /**
         * Set the version of the API.
         *
         * @param version The version.
         * @return This builder.
         */
        @JsonProperty("version")
        public Builder version(String version) {
            this.version = version;
            return this;
        }

        /**
         * Builds the ApiDescription instance.
         *
         * @return ApiDescription instance
         */
        public ApiDescription build() {
            return new ApiDescription(this);
        }
    }
}