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 2010–2011 ApexIdentity Inc.
015 * Portions Copyright 2011-2014 ForgeRock AS.
016 */
017
018package org.forgerock.openig.filter;
019
020import static org.forgerock.openig.util.Json.*;
021
022import java.io.IOException;
023import java.net.URISyntaxException;
024import java.util.List;
025import java.util.Map;
026
027import org.forgerock.json.fluent.JsonValue;
028import org.forgerock.openig.el.Expression;
029import org.forgerock.openig.handler.Handler;
030import org.forgerock.openig.handler.HandlerException;
031import org.forgerock.openig.heap.GenericHeaplet;
032import org.forgerock.openig.heap.HeapException;
033import org.forgerock.openig.http.Exchange;
034import org.forgerock.openig.http.Form;
035import org.forgerock.openig.http.Request;
036import org.forgerock.openig.util.CaseInsensitiveMap;
037import org.forgerock.openig.util.MultiValueMap;
038
039/**
040 * Creates a new request with in the exchange object. It will replace any request that may
041 * already be present in the exchange. The request can include a form, specified in the
042 * {@code form} field, which is included in an entity encoded in
043 * {@code application/x-www-form-urlencoded} format if request method is {@code POST}, or
044 * otherwise as (additional) query parameters in the URI.
045 */
046public class StaticRequestFilter extends GenericFilter {
047
048    /**
049     * By default, do not restore the original {@link Request} back into {@code exchange.request}.
050     */
051    public static final boolean DEFAULT_RESTORE = false;
052
053    /** The HTTP method to be performed on the resource. */
054    private final String method;
055
056    /** URI as an expression to allow dynamic URI construction. */
057    private Expression uri;
058
059    /** Protocol version (e.g. {@code "HTTP/1.1"}). */
060    private String version;
061
062    /** Restore the original Request after execution (defaults to {@literal false}). */
063    private boolean restore = DEFAULT_RESTORE;
064
065    /** Message header fields whose values are expressions that are evaluated. */
066    private final MultiValueMap<String, Expression> headers =
067            new MultiValueMap<String, Expression>(new CaseInsensitiveMap<List<Expression>>());
068
069    /** A form to include in the request, whose values are exchange-scoped expressions that are evaluated. */
070    private final MultiValueMap<String, Expression> form =
071            new MultiValueMap<String, Expression>(new CaseInsensitiveMap<List<Expression>>());
072
073    /**
074     * Builds a new {@link StaticRequestFilter} that will uses the given HTTP method on the resource.
075     *
076     * @param method
077     *         The HTTP method to be performed on the resource
078     */
079    public StaticRequestFilter(final String method) {
080        this.method = method;
081    }
082
083    /**
084     * Sets the target URI as an expression to allow dynamic URI construction.
085     *
086     * @param uri
087     *         target URI expression
088     */
089    public void setUri(final Expression uri) {
090        this.uri = uri;
091    }
092
093    /**
094     * Sets the new request message's version.
095     *
096     * @param version
097     *         Protocol version (e.g. {@code "HTTP/1.1"}).
098     */
099    public void setVersion(final String version) {
100        this.version = version;
101    }
102
103    /**
104     * Sets to {@literal false} if this filter should not restore the original Request after execution.
105     *
106     * @param restore
107     *         {@literal true} if restore is required, {@literal false} otherwise
108     */
109    public void setRestore(final boolean restore) {
110        this.restore = restore;
111    }
112
113    /**
114     * Adds a new header value using the given {@code key} with the given {@link Expression}. As headers are
115     * multi-valued objects, it's perfectly legal to call this method multiple times with the same key.
116     *
117     * @param key
118     *         Header name
119     * @param value
120     *         {@link Expression} that represents the value of the new header
121     * @return this object for fluent usage
122     */
123    public StaticRequestFilter addHeaderValue(final String key, final Expression value) {
124        headers.add(key, value);
125        return this;
126    }
127
128    /**
129     * Adds a new form parameter using the given {@code key} with the given {@link Expression}. As form parameters are
130     * multi-valued objects, it's perfectly legal to call this method multiple times with the same key.
131     *
132     * @param name
133     *         Form parameter name
134     * @param value
135     *         {@link Expression} that represents the value of the parameter
136     * @return this object for fluent usage
137     */
138    public StaticRequestFilter addFormParameter(final String name, final Expression value) {
139        form.add(name, value);
140        return this;
141    }
142
143    @Override
144    public void filter(Exchange exchange, Handler next) throws HandlerException, IOException {
145        Request request = new Request();
146        request.setMethod(this.method);
147        String value = this.uri.eval(exchange, String.class);
148        if (value != null) {
149            try {
150                request.setUri(value);
151            } catch (URISyntaxException e) {
152                throw logger.debug(new HandlerException("The URI " + value + " was not valid, " + e.getMessage(), e));
153            }
154        } else {
155            throw logger.debug(new HandlerException("The URI expression evaluated to null"));
156        }
157        if (this.version != null) {
158            // default in Message class
159            request.setVersion(version);
160        }
161        for (String key : this.headers.keySet()) {
162            for (Expression expression : this.headers.get(key)) {
163                String eval = expression.eval(exchange, String.class);
164                if (eval != null) {
165                    request.getHeaders().add(key, eval);
166                }
167            }
168        }
169        if (this.form != null && !this.form.isEmpty()) {
170            Form f = new Form();
171            for (String key : this.form.keySet()) {
172                for (Expression expression : this.form.get(key)) {
173                    String eval = expression.eval(exchange, String.class);
174                    if (eval != null) {
175                        f.add(key, eval);
176                    }
177                }
178            }
179            if (request.getMethod().equals("POST")) {
180                f.toRequestEntity(request);
181            } else {
182                f.appendRequestQuery(request);
183            }
184        }
185        Request saved = exchange.request;
186        exchange.request = request;
187        next.handle(exchange);
188        if (restore) {
189            exchange.request = saved;
190        }
191    }
192
193    /** Creates and initializes a request filter in a heap environment. */
194    public static class Heaplet extends GenericHeaplet {
195        @Override
196        public Object create() throws HeapException {
197            StaticRequestFilter filter = new StaticRequestFilter(config.get("method").required().asString());
198            filter.setUri(asExpression(config.get("uri").required()));
199            filter.setVersion(config.get("version").asString());
200            filter.setRestore(config.get("restore").defaultTo(DEFAULT_RESTORE).asBoolean());
201
202            JsonValue headers = config.get("headers").expect(Map.class);
203            if (headers != null) {
204                for (String key : headers.keys()) {
205                    for (JsonValue value : headers.get(key).required().expect(List.class)) {
206                        filter.addHeaderValue(key, asExpression(value.required()));
207                    }
208                }
209            }
210            JsonValue form = config.get("form").expect(Map.class);
211            if (form != null) {
212                for (String key : form.keys()) {
213                    for (JsonValue value : form.get(key).required().expect(List.class)) {
214                        filter.addFormParameter(key, asExpression(value.required()));
215                    }
216                }
217            }
218            return filter;
219        }
220    }
221}