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-2015 ForgeRock AS.
016 */
017
018package org.forgerock.openig.filter;
019
020import static org.forgerock.openig.el.Bindings.bindings;
021import static org.forgerock.openig.util.JsonValues.asExpression;
022
023import java.util.ArrayList;
024import java.util.List;
025import java.util.Map;
026
027import org.forgerock.http.Filter;
028import org.forgerock.http.Handler;
029import org.forgerock.http.protocol.Request;
030import org.forgerock.http.protocol.Response;
031import org.forgerock.json.JsonValue;
032import org.forgerock.openig.el.Bindings;
033import org.forgerock.openig.el.Expression;
034import org.forgerock.openig.heap.GenericHeapObject;
035import org.forgerock.openig.heap.GenericHeaplet;
036import org.forgerock.openig.heap.HeapException;
037import org.forgerock.services.context.Context;
038import org.forgerock.util.AsyncFunction;
039import org.forgerock.util.promise.NeverThrowsException;
040import org.forgerock.util.promise.Promise;
041import org.forgerock.util.promise.Promises;
042
043/**
044 * Conditionally diverts the request to another handler. Before and after the request is
045 * handled, associated conditions are evaluated. If a condition evaluates to {@code true}, then
046 * the processing flow is diverted to the associated handler. If no condition evaluates to
047 * {@code true}, then the request flows normally through the filter.
048 */
049public class SwitchFilter extends GenericHeapObject implements Filter {
050
051    /** Associates a condition with a handler to divert to if the condition yields {@code true}. */
052    private static class Case {
053        /** Condition to evaluate if request should be diverted to handler. */
054        private final Expression<Boolean> condition;
055
056        /** Handler to divert to if condition yields {@code true}. */
057        private final Handler handler;
058
059        /**
060         * Build a switch case from a condition and the handler to execute if condition yields.
061         * @param condition expression to evaluate
062         * @param handler handler to be executed if the condition yields
063         */
064        public Case(final Expression<Boolean> condition, final Handler handler) {
065            this.condition = condition;
066            this.handler = handler;
067        }
068    }
069
070    /** Switch cases to test before the request is handled. */
071    private final List<Case> requestCases = new ArrayList<>();
072
073    /** Switch cases to test after the request is handled. */
074    private final List<Case> responseCases = new ArrayList<>();
075
076    /**
077     * Add a request switch case with a condition and the handler to execute if condition yields.
078     * @param condition expression to evaluate
079     * @param handler handler to be executed if the condition yields
080     * @return this filter for fluent invocation.
081     */
082    public SwitchFilter addRequestCase(final Expression<Boolean> condition, final Handler handler) {
083        requestCases.add(new Case(condition, handler));
084        return this;
085    }
086
087    /**
088     * Add a response switch case with a condition and the handler to execute if condition yields.
089     * @param condition expression to evaluate
090     * @param handler handler to be executed if the condition yields
091     * @return this filter for fluent invocation.
092     */
093    public SwitchFilter addResponseCase(final Expression<Boolean> condition, final Handler handler) {
094        responseCases.add(new Case(condition, handler));
095        return this;
096    }
097
098    @Override
099    public Promise<Response, NeverThrowsException> filter(final Context context,
100                                                          final Request request,
101                                                          final Handler next) {
102        // Switch on the request flow
103        Promise<Response, NeverThrowsException> promise = doSwitch(bindings(context, request),
104                                                                   context,
105                                                                   request,
106                                                                   requestCases);
107        if (promise != null) {
108            return promise;
109        }
110        // not intercepted on request
111        // Invoke next filter in chain and try switching on the response flow
112        return next.handle(context, request)
113                .thenAsync(new AsyncFunction<Response, Response, NeverThrowsException>() {
114                    @Override
115                    public Promise<Response, NeverThrowsException> apply(final Response value) {
116                        Promise<Response, NeverThrowsException> promise = doSwitch(bindings(context,
117                                                                                            request,
118                                                                                            value),
119                                                                                   context,
120                                                                                   request,
121                                                                                   responseCases);
122                        // not intercepted on response, just return the original response
123                        if (promise == null) {
124                            promise = Promises.newResultPromise(value);
125                        }
126                        return promise;
127                    }
128                });
129    }
130
131    private Promise<Response, NeverThrowsException> doSwitch(Bindings bindings,
132                                                             Context context,
133                                                             Request request,
134                                                             List<Case> cases) {
135        for (Case c : cases) {
136            if (c.condition == null || Boolean.TRUE.equals(c.condition.eval(bindings))) {
137                // switched flow
138                return c.handler.handle(context, request);
139            }
140        }
141        // no interception
142        return null;
143    }
144
145    /**
146     * Creates and initializes an expect filter in a heap environment.
147     */
148    public static class Heaplet extends GenericHeaplet {
149        @Override
150        public Object create() throws HeapException {
151            SwitchFilter result = new SwitchFilter();
152            result.requestCases.addAll(asCases("onRequest"));
153            result.responseCases.addAll(asCases("onResponse"));
154            return result;
155        }
156
157        private List<Case> asCases(String name) throws HeapException {
158            ArrayList<Case> result = new ArrayList<>();
159            JsonValue cases = config.get(name).expect(List.class);
160            for (JsonValue value : cases) {
161                result.add(asCase(value.required().expect(Map.class)));
162            }
163            return result;
164        }
165
166        private Case asCase(JsonValue value) throws HeapException {
167            return new Case(asExpression(value.get("condition"), Boolean.class),
168                            heap.resolve(value.get("handler"), Handler.class));
169        }
170    }
171}