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 */
017
018package org.forgerock.openig.el;
019
020import static java.util.Collections.unmodifiableMap;
021
022import java.util.HashMap;
023import java.util.LinkedHashMap;
024import java.util.Map;
025
026import org.forgerock.http.protocol.Request;
027import org.forgerock.http.protocol.Response;
028import org.forgerock.http.session.SessionContext;
029import org.forgerock.services.context.AttributesContext;
030import org.forgerock.services.context.Context;
031import org.forgerock.util.Reject;
032
033/**
034 * Bindings represents the Expression bindings used during evaluation and assignation.
035 */
036public class Bindings {
037
038    private final Map<String, Object> map = new LinkedHashMap<>();
039
040    /**
041     * Returns an empty {@link Bindings} instance (mutable).
042     *
043     * @return an empty {@link Bindings} instance.
044     */
045    public static Bindings bindings() {
046        return new Bindings();
047    }
048
049    /**
050     * Returns a {@link Bindings} initialized with the given {@code context} and {@code request}.
051     *
052     * <p>The returned bindings contain a {@code contexts} entry that provides easy access to visible parent
053     * Contexts ({@code contexts.http, contexts.client, ...}).
054     *
055     * <p>They also give access to the request's {@code attributes} from the
056     * {@link org.forgerock.services.context.AttributesContext} and to the {@code session}
057     * from the {@link org.forgerock.http.session.SessionContext}.
058     *
059     * @param context
060     *         The context to expose
061     * @param request
062     *         The request to expose
063     * @return an initialized {@link Bindings} instance.
064     */
065    public static Bindings bindings(Context context, Request request) {
066        Bindings bindings = bindings()
067                .bind("context", context)
068                .bind("request", request);
069        if (context != null) {
070            bindings.bind("contexts", flatten(context));
071            if (context.containsContext(AttributesContext.class)) {
072                bindings.bind("attributes", context.asContext(AttributesContext.class).getAttributes());
073            }
074            if (context.containsContext(SessionContext.class)) {
075                bindings.bind("session", context.asContext(SessionContext.class).getSession());
076            }
077        }
078        return bindings;
079    }
080
081    /**
082     * Returns a {@link Bindings} initialized with the given {@code context}, {@code request} and {@code response}.
083     *
084     * @param context
085     *         The context to expose
086     * @param request
087     *         The request to expose
088     * @param response
089     *         The response to expose
090     * @return an initialized {@link Bindings} instance.
091     */
092    public static Bindings bindings(Context context, Request request, Response response) {
093        return bindings(context, request)
094                .bind("response", response);
095    }
096
097    /**
098     * Returns a singleton {@link Bindings} initialized with the given {@code name} and {@code value}.
099     *
100     * @param name
101     *         binding name
102     * @param value
103     *         binding value
104     * @return an initialized {@link Bindings} instance.
105     */
106    public static Bindings bindings(final String name, final Object value) {
107        return bindings().bind(name, value);
108    }
109
110    /**
111     * Binds a new {@code value} to the given {@code name}.
112     *
113     * @param name
114     *         binding name (must not be {@code null})
115     * @param value
116     *         binding value
117     * @return this {@link Bindings}
118     */
119    public Bindings bind(String name, Object value) {
120        Reject.ifNull(name);
121        map.put(name, value);
122        return this;
123    }
124
125    /**
126     * Binds all the bindings already bound from {@code source}.
127     *
128     * @param source
129     *         current bindings to copy (must not be {@code null})
130     * @return this {@link Bindings}
131     */
132    public Bindings bind(Bindings source) {
133        Reject.ifNull(source);
134        map.putAll(source.map);
135        return this;
136    }
137
138    /**
139     * Returns an unmodifiable {@code Map} view of this {@code Bindings} instance.
140     * <p>
141     * Note that while the consumer of the Map cannot modify it, any changes done through the Bindings methods
142     * will be reflected in the returned Map.
143     *
144     * @return an unmodifiable {@code Map} view of this instance (never {@code null}).
145     */
146    public Map<String, Object> asMap() {
147        return unmodifiableMap(map);
148    }
149
150    /**
151     * Flatten the current {@code leaf} {@link Context} into a Map keyed by context name.
152     *
153     * <p>
154     * The Context hierarchy is walked from the given {@code leaf} to the root context, parent after parent.
155     *
156     * <p>
157     * If a context's name has already been added into the Map while walking up the chain, the context will be
158     * ignored (shadowed by the previously registered context). That behaviour ensure that we'll return only the
159     * contexts that are close to the leaf.
160     *
161     * @param leaf
162     *         Context used to start the walk-through
163     * @return a Map of context
164     * @see Context#getContextName()
165     */
166    public static Map<String, Context> flatten(Context leaf) {
167        Map<String, Context> contexts = new HashMap<>();
168        contexts.put(leaf.getContextName(), leaf);
169
170        Context context = leaf;
171        while (context.getParent() != null) {
172            context = context.getParent();
173            String name = context.getContextName();
174            if (!contexts.containsKey(name)) {
175                // Ignore already mapped contexts
176                contexts.put(name, context);
177            }
178        }
179
180        return contexts;
181    }
182
183    @Override
184    public String toString() {
185        return map.toString();
186    }
187
188}