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.routing;
018
019import java.util.List;
020
021import org.forgerock.http.Filter;
022import org.forgerock.http.header.AcceptApiVersionHeader;
023import org.forgerock.http.protocol.Request;
024import org.forgerock.http.util.Paths;
025import org.forgerock.services.context.Context;
026import org.forgerock.services.routing.RouteMatch;
027import org.forgerock.services.routing.RouteMatcher;
028
029/**
030 * A utility class that contains methods for creating route matchers.
031 */
032public final class RouteMatchers {
033
034    private RouteMatchers() {
035        // Private utility constructor.
036    }
037
038    /**
039     * Creates a {@code RouteMatcher} instance that matches
040     * {@code ResourcePath}s with the provided {@literal mode} and
041     * {@literal template}.
042     *
043     * @param mode The routing mode.
044     * @param template The uri template.
045     * @return A {@code RouteMatcher} instance.
046     */
047    public static RouteMatcher<List<String>> uriMatcher(RoutingMode mode, String template) {
048        return new UriRouteMatcher(mode, template);
049    }
050
051    /**
052     * Creates a {@code RouteMatcher} instance that matches {@code Request}s
053     * with the provided {@literal mode} and {@literal template}.
054     *
055     * @param mode The routing mode.
056     * @param template The uri template.
057     * @return A {@code RouteMatcher} instance.
058     */
059    public static RouteMatcher<Request> requestUriMatcher(RoutingMode mode, String template) {
060        RouteMatcher<List<String>> delegate = uriMatcher(mode, template);
061        return new RequestUriRouteMatcher(delegate);
062    }
063
064    /**
065     * Creates a new {@code ResourceApiVersionBehaviourManager} which is responsibly
066     * for managing whether warning headers are returned and the default
067     * version behaviour when the {@literal Accept-API-Version} header is not
068     * present on the request.
069     *
070     * @return A new {@code ResourceApiVersionBehaviourManager}.
071     */
072    public static ResourceApiVersionBehaviourManager newResourceApiVersionBehaviourManager() {
073        return new ResourceApiVersionBehaviourManagerImpl();
074    }
075
076    /**
077     * Creates a {@code Filter} which MUST be placed, in the route, before any
078     * API Version routing takes place.
079     *
080     * <p>The filter will add the required {@code Context}s, default version
081     * behaviour and response headers.</p>
082     *
083     * @param behaviourManager A {@code ResourceApiVersionBehaviourManager} instance.
084     * @return A {@code Filter} instance.
085     */
086    public static Filter resourceApiVersionContextFilter(ResourceApiVersionBehaviourManager behaviourManager) {
087        return new ResourceApiVersionRoutingFilter(behaviourManager);
088    }
089
090    /**
091     * Creates a {@code RouteMatcher} instance that matches the request
092     * resource API version with the provided {@literal version}.
093     *
094     * @param version The API version of the resource.
095     * @return A {@code RouteMatcher} instance.
096     */
097    public static RouteMatcher<Version> resourceApiVersionMatcher(Version version) {
098        return new ResourceApiVersionRouteMatcher(version);
099    }
100
101    /**
102     * Creates a {@code RouteMatcher} instance that matches the request
103     * resource API version with the provided {@literal version}.
104     *
105     * @param version The API version of the resource.
106     * @return A {@code RouteMatcher} instance.
107     */
108    public static RouteMatcher<Request> requestResourceApiVersionMatcher(Version version) {
109        return new RequestApiVersionRouteMatcher(resourceApiVersionMatcher(version));
110    }
111
112    /**
113     * A CHF specific {@code RouteMatcher} which extracts the routable path
114     * from a {@code Request} and passes it as a {@code ResourcePath} to the
115     * common {@code ResourcePath} route matcher.
116     */
117    private static final class RequestUriRouteMatcher extends RouteMatcher<Request> {
118
119        private final RouteMatcher<List<String>> delegate;
120
121        private RequestUriRouteMatcher(RouteMatcher<List<String>> delegate) {
122            this.delegate = delegate;
123        }
124
125        @Override
126        public RouteMatch evaluate(Context context, Request request) {
127            return delegate.evaluate(context, getRemainingRequestUri(context, request));
128        }
129
130        @Override
131        public String toString() {
132            return delegate.toString();
133        }
134
135        @Override
136        public boolean equals(Object o) {
137            if (this == o) {
138                return true;
139            }
140            if (!(o instanceof RequestUriRouteMatcher)) {
141                return false;
142            }
143            RequestUriRouteMatcher that = (RequestUriRouteMatcher) o;
144            return delegate.equals(that.delegate);
145        }
146
147        @Override
148        public int hashCode() {
149            return delegate.hashCode();
150        }
151    }
152
153    static List<String> getRemainingRequestUri(Context context, Request request) {
154        List<String> path = request.getUri().getPathElements();
155        if (context.containsContext(UriRouterContext.class)) {
156            List<String> matchedUri = Paths.getPathElements(context.asContext(UriRouterContext.class).getBaseUri());
157            path = path.subList(matchedUri.size(), path.size());
158        }
159        return path;
160    }
161
162    /**
163     * The default implementation of the {@code ResourceApiVersionBehaviourManager} interface.
164     */
165    private static final class ResourceApiVersionBehaviourManagerImpl implements ResourceApiVersionBehaviourManager {
166
167        private boolean warningEnabled = true;
168        private DefaultVersionBehaviour defaultVersionBehaviour = DefaultVersionBehaviour.LATEST;
169
170        @Override
171        public void setWarningEnabled(boolean warningEnabled) {
172            this.warningEnabled = warningEnabled;
173        }
174
175        @Override
176        public boolean isWarningEnabled() {
177            return warningEnabled;
178        }
179
180        @Override
181        public void setDefaultVersionBehaviour(DefaultVersionBehaviour behaviour) {
182            this.defaultVersionBehaviour = behaviour;
183        }
184
185        @Override
186        public DefaultVersionBehaviour getDefaultVersionBehaviour() {
187            return defaultVersionBehaviour;
188        }
189    }
190
191    /**
192     * A CHF specific {@code RouteMatcher} which extracts the resource API
193     * version from a {@code Request} and passes it to the common
194     * {@code Version} route matcher.
195     */
196    private static final class RequestApiVersionRouteMatcher extends RouteMatcher<Request> {
197
198        private final RouteMatcher<Version> delegate;
199
200        private RequestApiVersionRouteMatcher(RouteMatcher<Version> delegate) {
201            this.delegate = delegate;
202        }
203
204        @Override
205        public RouteMatch evaluate(Context context, Request request) {
206            AcceptApiVersionHeader apiVersionHeader;
207            try {
208                apiVersionHeader = AcceptApiVersionHeader.valueOf(request);
209            } catch (IllegalArgumentException ignored) {
210                // This exception is thrown when trying to parse an invalid
211                // Accept-API-Version header. null is returned to signify a bad
212                // request.
213                return null;
214            }
215            return delegate.evaluate(context, apiVersionHeader.getResourceVersion());
216        }
217
218        @Override
219        public String toString() {
220            return delegate.toString();
221        }
222
223        @Override
224        public boolean equals(Object o) {
225            if (this == o) {
226                return true;
227            }
228            if (!(o instanceof RequestApiVersionRouteMatcher)) {
229                return false;
230            }
231            RequestApiVersionRouteMatcher that = (RequestApiVersionRouteMatcher) o;
232            return delegate.equals(that.delegate);
233        }
234
235        @Override
236        public int hashCode() {
237            return delegate.hashCode();
238        }
239    }
240}