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

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import javax.security.auth.Subject;

/**
 * A simple immutable value object class holding customizable SASL extensions.
 *
 * <p/>
 *
 * <b>Note on object identity and equality</b>: <code>SaslExtensions</code> <em>intentionally</em>
 * overrides the standard {@link #equals(Object)} and {@link #hashCode()} methods calling their
 * respective {@link Object#equals(Object)} and {@link Object#hashCode()} implementations. In so
 * doing, it provides equality <em>only</em> via reference identity and will not base equality on
 * the underlying values of its {@link #extensionsMap extensions map}.
 *
 * <p/>
 *
 * The reason for this approach to equality is based off of the manner in which
 * credentials are stored in a {@link Subject}. <code>SaslExtensions</code> are added to and
 * removed from a {@link Subject} via its {@link Subject#getPublicCredentials() public credentials}.
 * The public credentials are stored in a {@link Set} in the {@link Subject}, so object equality
 * therefore becomes a concern. With shallow, reference-based equality, distinct
 * <code>SaslExtensions</code> instances with the same map values can be considered unique. This is
 * critical to operations like token refresh.
 *
 * See <a href="https://issues.apache.org/jira/browse/KAFKA-14062">KAFKA-14062</a> for more detail.
 */
public class SaslExtensions {
    private final Map<String, String> extensionsMap;

    public SaslExtensions(Map<String, String> extensionsMap) {
        this.extensionsMap = Collections.unmodifiableMap(new HashMap<>(extensionsMap));
    }

    /**
     * Returns an <strong>immutable</strong> map of the extension names and their values
     */
    public Map<String, String> map() {
        return extensionsMap;
    }

    /**
     * Creates an "empty" instance indicating no SASL extensions. <em>Do not cache the result of
     * this method call</em> for use by multiple {@link Subject}s as the references need to be
     * unique.
     *
     * <p/>
     *
     * See the class-level documentation for details.
     * @return Unique, but empty, <code>SaslExtensions</code> instance
     */
    @SuppressWarnings("unchecked")
    public static SaslExtensions empty() {
        // It's ok to re-use the EMPTY_MAP instance as the object equality is on the outer
        // SaslExtensions reference.
        return new SaslExtensions(Collections.EMPTY_MAP);
    }

    /**
     * Implements equals using the reference comparison implementation from
     * {@link Object#equals(Object)}.
     *
     * <p/>
     *
     * See the class-level documentation for details.
     *
     * @param o Other object to compare
     * @return True if <code>o == this</code>
     */
    @Override
    public final boolean equals(Object o) {
        return super.equals(o);
    }

    /**
     * Implements <code>hashCode</code> using the native implementation from
     * {@link Object#hashCode()}.
     *
     * <p/>
     *
     * See the class-level documentation for details.
     *
     * @return Hash code of instance
     */
    @Override
    public final int hashCode() {
        return super.hashCode();
    }

    @Override
    public String toString() {
        return new StringJoiner(", ", SaslExtensions.class.getSimpleName() + "[", "]")
            .add("extensionsMap=" + extensionsMap)
            .toString();
    }

}
