001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2015 ForgeRock AS.
015 */
016
017package org.forgerock.http.header;
018
019import static java.util.Collections.*;
020import static org.forgerock.http.header.HeaderUtil.*;
021import static org.forgerock.http.routing.Version.*;
022
023import java.util.List;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.forgerock.http.protocol.Header;
028import org.forgerock.http.protocol.Message;
029import org.forgerock.http.routing.Version;
030import org.forgerock.util.Pair;
031
032/**
033 * Processes the <strong>{@code Accept-API-Version}</strong> message header.
034 * Represents the accepted protocol and resource versions.
035 */
036public final class AcceptApiVersionHeader extends Header {
037
038    /**
039     * Constructs a new header, initialized from the specified message.
040     *
041     * @param message The message to initialize the header from.
042     * @return The parsed header.
043     * @throws IllegalArgumentException If the version header is in an invalid format.
044     */
045    public static AcceptApiVersionHeader valueOf(Message message) {
046        return valueOf(parseSingleValuedHeader(message, NAME));
047    }
048
049    /**
050     * Constructs a new header, initialized from the specified string value.
051     *
052     * @param string The value to initialize the header from.
053     * @return The parsed header.
054     * @throws IllegalArgumentException If the version header is in an invalid format.
055     */
056    public static AcceptApiVersionHeader valueOf(String string) {
057        Pair<Version, Version> parsedValue = parse(string);
058        return new AcceptApiVersionHeader(parsedValue.getFirst(), parsedValue.getSecond());
059    }
060
061    static Pair<Version, Version> parse(String string) {
062        if (string == null || string.isEmpty()) {
063            return Pair.empty();
064        }
065
066        Matcher matcher = EXPECTED_VERSION_FORMAT.matcher(string);
067        if (!matcher.matches()) {
068            throw new IllegalArgumentException("Version string is in an invalid format: " + string);
069        }
070
071        Version protocolVersion = null;
072        Version resourceVersion = null;
073        if (matcher.group(1) != null) {
074            protocolVersion = version(matcher.group(1));
075        } else if (matcher.group(3) != null) {
076            resourceVersion = version(matcher.group(3));
077        } else if (matcher.group(5) != null) {
078            protocolVersion = version(matcher.group(5));
079            resourceVersion = version(matcher.group(7));
080        } else {
081            resourceVersion = version(matcher.group(9));
082            protocolVersion = version(matcher.group(11));
083        }
084        return Pair.of(protocolVersion, resourceVersion);
085    }
086
087    /** The name of this header. */
088    public static final String NAME = "Accept-API-Version";
089    private static final String PROTOCOL = "protocol";
090    private static final String RESOURCE = "resource";
091    private static final String EQUALS = "=";
092
093    /**
094     * Regex that accepts a comma separated protocol and resource version.
095     * The version {@code String}s can either by {@code Integer}s or {@code Double}s.
096     * It is invalid to contain neither a protocol or resource version.
097     *
098     * Pattern matches:
099     * <ul>
100     *     <li>protocol=123.123,resource=123.123</li>
101     *     <li>protocol=123,resource=123</li>
102     *     <li>protocol=123, resource=123</li>
103     *     <li>protocol=123.123</li>
104     *     <li>protocol=123</li>
105     *     <li>resource=123.123</li>
106     *     <li>resource=123</li>
107     *     <li>resource=123.123,protocol=123.123</li>
108     * </ul>
109     *
110     * Pattern does not matches:
111     * <ul>
112     *     <li>protocol=123.123.123,resource=123.123.123</li>
113     *     <li>protocol=123.123resource=123.123</li>
114     *     <li>protocol=123.123 resource=123.123</li>
115     *     <li>protocol= resource=</li>
116     * </ul>
117     */
118    private static final String PROTOCOL_VERSION_REGEX = PROTOCOL + "=(\\d+(\\.\\d+)?)";
119    private static final String RESOURCE_VERSION_REGEX = RESOURCE + "=(\\d+(\\.\\d+)?)";
120    private static final Pattern EXPECTED_VERSION_FORMAT =
121            Pattern.compile("^" + PROTOCOL_VERSION_REGEX + "$"
122                                    + "|^" + RESOURCE_VERSION_REGEX + "$"
123                                    + "|^" + PROTOCOL_VERSION_REGEX + "\\s*,\\s*" + RESOURCE_VERSION_REGEX + "$"
124                                    + "|^" + RESOURCE_VERSION_REGEX + "\\s*,\\s*" + PROTOCOL_VERSION_REGEX + "$");
125
126    private Version protocolVersion;
127    private Version resourceVersion;
128
129    /**
130     * Constructs a new header, initialized with the specified protocol and resource versions.
131     *
132     * @param protocol The accepted protocol version.
133     * @param resource The accepted resource version.
134     */
135    public AcceptApiVersionHeader(Version protocol, Version resource) {
136        this.protocolVersion = protocol;
137        this.resourceVersion = resource;
138    }
139
140    @Override
141    public String getName() {
142        return NAME;
143    }
144
145    /**
146     * Gets the acceptable protocol version.
147     *
148     * @return The acceptable protocol version.
149     */
150    public Version getProtocolVersion() {
151        return protocolVersion;
152    }
153
154    /**
155     * Gets the acceptable resource version.
156     *
157     * @return The acceptable resource version.
158     */
159    public Version getResourceVersion() {
160        return resourceVersion;
161    }
162
163    /**
164     * Will set the accepted protocol version, if not provided in the
165     * {@literal Accept-API-Version} header.
166     *
167     * @param version The default protocol version.
168     * @return The accept api version header.
169     */
170    public AcceptApiVersionHeader withDefaultProtocolVersion(Version version) {
171        if (protocolVersion == null && version != null) {
172            this.protocolVersion = version;
173        }
174        return this;
175    }
176
177    /**
178     * Will set the accepted resource version, if not provided in the
179     * {@literal Accept-API-Version} header.
180     *
181     * @param version The default resource version.
182     * @return The accept api version header.
183     */
184    public AcceptApiVersionHeader withDefaultResourceVersion(Version version) {
185        if (resourceVersion == null && version != null) {
186            this.resourceVersion = version;
187        }
188        return this;
189    }
190
191    @Override
192    public List<String> getValues() {
193        StringBuilder sb = new StringBuilder();
194        if (protocolVersion != null) {
195            sb.append(PROTOCOL).append(EQUALS).append(protocolVersion.toString());
196        }
197        if (protocolVersion != null && resourceVersion != null) {
198            sb.append(",");
199        }
200        if (resourceVersion != null) {
201            sb.append(RESOURCE).append(EQUALS).append(resourceVersion.toString());
202        }
203        return singletonList(sb.toString());
204    }
205
206    static class Factory extends AbstractSingleValuedHeaderFactory<AcceptApiVersionHeader> {
207
208        @Override
209        public AcceptApiVersionHeader parse(String value) {
210            return valueOf(value);
211        }
212
213    }
214
215}