AcceptApiVersionHeader.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 2015-2016 ForgeRock AS.
 */

package org.forgerock.http.header;

import static java.util.Collections.*;
import static org.forgerock.http.header.HeaderUtil.*;
import static org.forgerock.http.routing.Version.*;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.forgerock.http.protocol.Header;
import org.forgerock.http.protocol.Message;
import org.forgerock.http.routing.Version;
import org.forgerock.util.Pair;

/**
 * Processes the <strong>{@code Accept-API-Version}</strong> message header.
 * Represents the accepted protocol and resource versions.
 */
public final class AcceptApiVersionHeader extends Header {

    /**
     * Constructs a new header, initialized from the specified message.
     *
     * @param message The message to initialize the header from.
     * @return The parsed header.
     * @throws IllegalArgumentException If the version header is in an invalid format.
     */
    public static AcceptApiVersionHeader valueOf(Message message) {
        return valueOf(parseSingleValuedHeader(message, NAME));
    }

    /**
     * Constructs a new header, initialized from the specified string value.
     *
     * @param string The value to initialize the header from.
     * @return The parsed header.
     * @throws IllegalArgumentException If the version header is in an invalid format.
     */
    public static AcceptApiVersionHeader valueOf(String string) {
        Pair<Version, Version> parsedValue = parse(string);
        return new AcceptApiVersionHeader(parsedValue.getFirst(), parsedValue.getSecond());
    }

    static Pair<Version, Version> parse(String string) {
        if (string == null || string.isEmpty()) {
            return Pair.empty();
        }

        Matcher matcher = EXPECTED_VERSION_FORMAT.matcher(string);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("Version string is in an invalid format: " + string);
        }

        Version protocolVersion = null;
        Version resourceVersion = null;
        if (matcher.group(1) != null) {
            protocolVersion = version(matcher.group(1));
        } else if (matcher.group(3) != null) {
            resourceVersion = version(matcher.group(3));
        } else if (matcher.group(5) != null) {
            protocolVersion = version(matcher.group(5));
            resourceVersion = version(matcher.group(7));
        } else {
            resourceVersion = version(matcher.group(9));
            protocolVersion = version(matcher.group(11));
        }
        return Pair.of(protocolVersion, resourceVersion);
    }

    /** The name of this header. */
    public static final String NAME = "Accept-API-Version";
    /** The name of the protocol value component. */
    public static final String PROTOCOL = "protocol";
    /** The name of the resource value component. */
    public static final String RESOURCE = "resource";
    private static final String EQUALS = "=";

    /**
     * Regex that accepts a comma separated protocol and resource version.
     * The version {@code String}s can either by {@code Integer}s or {@code Double}s.
     * It is invalid to contain neither a protocol or resource version.
     *
     * Pattern matches:
     * <ul>
     *     <li>protocol=123.123,resource=123.123</li>
     *     <li>protocol=123,resource=123</li>
     *     <li>protocol=123, resource=123</li>
     *     <li>protocol=123.123</li>
     *     <li>protocol=123</li>
     *     <li>resource=123.123</li>
     *     <li>resource=123</li>
     *     <li>resource=123.123,protocol=123.123</li>
     * </ul>
     *
     * Pattern does not matches:
     * <ul>
     *     <li>protocol=123.123.123,resource=123.123.123</li>
     *     <li>protocol=123.123resource=123.123</li>
     *     <li>protocol=123.123 resource=123.123</li>
     *     <li>protocol= resource=</li>
     * </ul>
     */
    private static final String PROTOCOL_VERSION_REGEX = PROTOCOL + "=(\\d+(\\.\\d+)?)";
    private static final String RESOURCE_VERSION_REGEX = RESOURCE + "=(\\d+(\\.\\d+)?)";
    private static final Pattern EXPECTED_VERSION_FORMAT =
            Pattern.compile("^" + PROTOCOL_VERSION_REGEX + "$"
                                    + "|^" + RESOURCE_VERSION_REGEX + "$"
                                    + "|^" + PROTOCOL_VERSION_REGEX + "\\s*,\\s*" + RESOURCE_VERSION_REGEX + "$"
                                    + "|^" + RESOURCE_VERSION_REGEX + "\\s*,\\s*" + PROTOCOL_VERSION_REGEX + "$");

    private Version protocolVersion;
    private Version resourceVersion;

    /**
     * Constructs a new header, initialized with the specified protocol and resource versions.
     *
     * @param protocol The accepted protocol version.
     * @param resource The accepted resource version.
     */
    public AcceptApiVersionHeader(Version protocol, Version resource) {
        this.protocolVersion = protocol;
        this.resourceVersion = resource;
    }

    @Override
    public String getName() {
        return NAME;
    }

    /**
     * Gets the acceptable protocol version.
     *
     * @return The acceptable protocol version.
     */
    public Version getProtocolVersion() {
        return protocolVersion;
    }

    /**
     * Gets the acceptable resource version.
     *
     * @return The acceptable resource version.
     */
    public Version getResourceVersion() {
        return resourceVersion;
    }

    /**
     * Will set the accepted protocol version, if not provided in the
     * {@literal Accept-API-Version} header.
     *
     * @param version The default protocol version.
     * @return The accept api version header.
     */
    public AcceptApiVersionHeader withDefaultProtocolVersion(Version version) {
        if (protocolVersion == null && version != null) {
            this.protocolVersion = version;
        }
        return this;
    }

    /**
     * Will set the accepted resource version, if not provided in the
     * {@literal Accept-API-Version} header.
     *
     * @param version The default resource version.
     * @return The accept api version header.
     */
    public AcceptApiVersionHeader withDefaultResourceVersion(Version version) {
        if (resourceVersion == null && version != null) {
            this.resourceVersion = version;
        }
        return this;
    }

    @Override
    public List<String> getValues() {
        StringBuilder sb = new StringBuilder();
        if (protocolVersion != null) {
            sb.append(PROTOCOL).append(EQUALS).append(protocolVersion.toString());
        }
        if (protocolVersion != null && resourceVersion != null) {
            sb.append(",");
        }
        if (resourceVersion != null) {
            sb.append(RESOURCE).append(EQUALS).append(resourceVersion.toString());
        }
        return singletonList(sb.toString());
    }

    static class Factory extends AbstractSingleValuedHeaderFactory<AcceptApiVersionHeader> {

        @Override
        public AcceptApiVersionHeader parse(String value) {
            return valueOf(value);
        }

    }

}