ApiError.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.util.ValidationUtil.isEmpty;

import java.util.Comparator;
import java.util.Objects;

import org.forgerock.api.ApiValidationException;
import org.wrensecurity.guava.common.base.Strings;
import org.forgerock.util.i18n.LocalizableString;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

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

    /**
     * {@link ApiError} {@link Comparator}, which sorts by code and description.
     */
    public static final ErrorComparator ERROR_COMPARATOR = new ErrorComparator();

    // Must be an Integer, because 0 is not a valid default
    private final Integer code;
    private final LocalizableString description;
    private final Schema schema;
    @JsonProperty("$ref")
    private final Reference reference;

    private ApiError(Builder builder) {
        this.code = builder.code;
        this.description = builder.description;
        this.schema = builder.schema;
        this.reference = builder.reference;

        if (reference == null && (code == null || description == null || isEmpty(description.toString()))) {
            throw new ApiValidationException("code and description are required");
        }
        if (reference != null && (code != null || description != null || schema != null)) {
            throw new ApiValidationException("Cannot set code, description or schema when using a reference");
        }
    }

    /**
     * Getter of the error code.
     *
     * @return Code
     */
    public Integer getCode() {
        return code;
    }

    /**
     * Getter of the error description.
     *
     * @return Description
     */
    public LocalizableString getDescription() {
        return description;
    }

    /**
     * Getter of the error schema.
     *
     * @return Schema
     */
    public Schema getSchema() {
        return schema;
    }

    /**
     * Getter of the reference.
     *
     * @return The reference.
     */
    public Reference getReference() {
        return reference;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        ApiError apiError = (ApiError) o;
        return Objects.equals(code, apiError.code)
                && Objects.equals(description, apiError.description)
                && Objects.equals(schema, apiError.schema)
                && Objects.equals(reference, apiError.reference);
    }

    @Override
    public int hashCode() {
        return Objects.hash(code, description, schema, reference);
    }

    /**
     * New apiError builder.
     *
     * @return Builder
     */
    public static Builder apiError() {
        return new Builder();
    }

    /**
     * Builds an ApiError object from the data in the annotation. If the {@code ApiError} has an {@code id} defined, the
     * ApiError will be defined in the top-level {@code descriptor}, and a reference to that definition will be
     * returned.
     *
     * @param apiError The annotation that holds the data
     * @param descriptor The root descriptor, for adding definitions to.
     * @param relativeType The type relative to which schema resources should be resolved.
     * @return ApiError instance
     */
    public static ApiError fromAnnotation(org.forgerock.api.annotations.ApiError apiError,
                                          ApiDescription descriptor, Class<?> relativeType) {
        ApiError apiErrorDefinition = apiError()
                .description(new LocalizableString(apiError.description(), relativeType))
                .code(apiError.code())
                .schema(Schema.fromAnnotation(apiError.detailSchema(), descriptor, relativeType))
                .build();
        if (!Strings.isNullOrEmpty(apiError.id())) {
            // we've got an id for this apiApiError, so define it at the top level and return a reference.
            descriptor.addError(apiError.id(), apiErrorDefinition);
            return apiError().reference(Reference.reference().value("#/errors/" + apiError.id()).build()).build();
        } else {
            return apiErrorDefinition;
        }
    }

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

        // Must be an Integer, because 0 is not a valid default.
        private Integer code;
        private LocalizableString description;
        private Schema schema;
        private Reference reference;

        private Builder() {
        }

        /**
         * Set the error code.
         *
         * @param code The apiError code.
         * @return This builder.
         */
        @JsonProperty("code")
        public Builder code(Integer code) {
            this.code = code;
            return this;
        }

        /**
         * Set the error description.
         *
         * @param description ApiError description
         * @return This builder.
         */
        public Builder description(LocalizableString description) {
            this.description = description;
            return this;
        }

        /**
         * Set the error description.
         *
         * @param description ApiError description
         * @return This builder.
         */
        @JsonProperty("description")
        public Builder description(String description) {
            this.description = new LocalizableString(description);
            return this;
        }

        /**
         * Set the schema.
         *
         * @param schema ApiError schema
         * @return This builder.
         */
        @JsonProperty("schema")
        public Builder schema(Schema schema) {
            this.schema = schema;
            return this;
        }

        /**
         * Set the error as a reference to another definition.
         *
         * @param reference The reference.
         * @return This builder.
         */
        @JsonProperty("$ref")
        public Builder reference(Reference reference) {
            this.reference = reference;
            return this;
        }

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

    /**
     * {@link ApiError} {@link Comparator}, which sorts by code and description. This {@code Comparator} does not handle
     * {@code null} values or duplicates, because those conditions should never occur in practice.
     * <p>
     * This class is thread-safe.
     * </p>
     */
    private static class ErrorComparator implements Comparator<ApiError> {
        @Override
        public int compare(final ApiError o1, final ApiError o2) {
            if (o1.getReference() != null) {
                return o2.getReference() != null
                        ? o1.getReference().getValue().compareTo(o2.getReference().getValue())
                        : 1;
            }
            if (o2.getReference() != null) {
                return -1;
            }
            final int codeCompare = o1.code.compareTo(o2.code);
            if (codeCompare == 0) {
                return o1.description.toString().compareTo(o2.description.toString());
            }
            return codeCompare;
        }
    }
}