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 2012-2015 ForgeRock AS.
015 */
016
017package org.forgerock.services.context;
018
019import static org.forgerock.json.JsonValue.json;
020import static org.forgerock.json.JsonValue.object;
021
022import java.lang.reflect.Constructor;
023
024import org.forgerock.json.JsonValue;
025import org.forgerock.util.Reject;
026
027/**
028 * A base implementation of the {@link Context} interface. Derived Contexts <b>MUST</b> support persistence by providing
029 * <ul>
030 * <li>a <b>public</b> constructor having the same declaration as
031 * {@link #AbstractContext(JsonValue, ClassLoader)}</li>
032 * <li>a <b>public</b> method having the same declaration as
033 * {@link Context#toJsonValue}</li>
034 * </ul>
035 * See the documentation for more details.
036 * <p>
037 * Here is an example of the JSON representation of the core attributes of all
038 * contexts:
039 *
040 * <pre>
041 * {
042 *   "id"     : "56f0fb7e-3837-464d-b9ec-9d3b6af665c3",
043 *   "class"  : ...Java class name..,
044 *   "parent" : {
045 *       ...
046 *   }
047 * }
048 * </pre>
049 */
050public abstract class AbstractContext implements Context {
051
052    // Persisted attribute names.
053    private static final String ATTR_CLASS = "class";
054    private static final String ATTR_ID = "id";
055    private static final String ATTR_NAME = "name";
056    private static final String ATTR_PARENT = "parent";
057
058    /**
059     * The parent Context.
060     */
061    private final Context parent;
062
063    /**
064     * The Context data.
065     */
066    protected final JsonValue data;
067
068    /**
069     * Constructs a new {@code AbstractContext} with a {@code null} {@code id}.
070     *
071     * @param parent The parent context.
072     * @param name The name of the context.
073     */
074    protected AbstractContext(Context parent, String name) {
075        this(null, name, parent);
076    }
077
078    /**
079     * Constructs a new {@code AbstractContext}.
080     *
081     * @param id The id of the context.
082     * @param parent The parent context.
083     * @param name The name of the context.
084     */
085    protected AbstractContext(String id, String name, Context parent) {
086        data = json(object());
087        data.put(ATTR_CLASS, getClass().getName());
088        if (id != null) {
089            data.put(ATTR_ID, id);
090        }
091        data.put(ATTR_NAME, name);
092        this.parent = parent;
093    }
094
095    /**
096     * Creates a new context from the JSON representation of a previously
097     * persisted context.
098     * <p>
099     * Sub-classes <b>MUST</b> provide a constructor having the same declaration
100     * as this constructor in order to support persistence. Implementations
101     * <b>MUST</b> take care to invoke the super class implementation before
102     * parsing their own context attributes. Below is an example implementation
103     * for a security context which stores the user name and password of the
104     * authenticated user:
105     *
106     * <pre>
107     * protected SecurityContext(JsonValue savedContext, ClassLoader classLoader) {
108     *     // Invoke the super-class implementation first.
109     *     super(savedContext, classLoader);
110     *
111     *     // Now parse the attributes for this context.
112     *     this.username = savedContext.get(&quot;username&quot;).required().asString();
113     *     this.password = savedContext.get(&quot;password&quot;).required().asString();
114     * }
115     * </pre>
116     *
117     * In order to create a context's persisted JSON representation,
118     * implementations must override.
119     *
120     * @param savedContext
121     *            The JSON representation from which this context's attributes
122     *            should be parsed.
123     * @param classLoader
124     *            The ClassLoader which can properly resolve the persisted class-name.
125     */
126    public AbstractContext(final JsonValue savedContext, final ClassLoader classLoader) {
127        final JsonValue savedParentContext = savedContext.get(ATTR_PARENT);
128        savedContext.remove(ATTR_PARENT);
129        data = savedContext.copy();
130        this.parent = savedParentContext.isNull() ? null : load0(savedParentContext, classLoader);
131    }
132
133    private static Context load0(final JsonValue savedContext, final ClassLoader classLoader) {
134        // Determine the context implementation class and instantiate it.
135        final String className = savedContext.get(ATTR_CLASS).required().asString();
136        try {
137            final Class<? extends Context> clazz = Class.forName(className, true, classLoader)
138                    .asSubclass(AbstractContext.class);
139            final Constructor<? extends Context> constructor = clazz.getDeclaredConstructor(
140                    JsonValue.class, ClassLoader.class);
141            return constructor.newInstance(savedContext, classLoader);
142        } catch (final Exception e) {
143            throw new IllegalArgumentException(
144                    "Unable to instantiate Context implementation class '" + className + "'", e);
145        }
146    }
147
148    @Override
149    public final String getContextName() {
150        return data.get(ATTR_NAME).asString();
151    }
152
153    @Override
154    public final <T extends Context> T asContext(Class<T> clazz) {
155        Reject.ifNull(clazz, "clazz cannot be null");
156        T context = asContext0(clazz);
157        if (context != null) {
158            return context;
159        } else {
160            throw new IllegalArgumentException("No context of type " + clazz.getName() + " found.");
161        }
162    }
163
164    @Override
165    public final Context getContext(String contextName) {
166        Context context = getContext0(contextName);
167        if (context != null) {
168            return context;
169        } else {
170            throw new IllegalArgumentException("No context of named " + contextName + " found.");
171        }
172    }
173
174    @Override
175    public final boolean containsContext(Class<? extends Context> clazz) {
176        return asContext0(clazz) != null;
177    }
178
179    @Override
180    public final boolean containsContext(String contextName) {
181        return getContext0(contextName) != null;
182    }
183
184    @Override
185    public final String getId() {
186        if (data.get(ATTR_ID).isNull() && !isRootContext()) {
187            return getParent().getId();
188        } else {
189            return data.get(ATTR_ID).required().asString();
190        }
191    }
192
193    @Override
194    public final Context getParent() {
195        return parent;
196    }
197
198    @Override
199    public final boolean isRootContext() {
200        return getParent() == null;
201    }
202
203    @Override
204    public JsonValue toJsonValue() {
205        final JsonValue value = data.copy();
206        value.put(ATTR_PARENT, parent != null ? parent.toJsonValue().getObject() : null);
207        return value;
208    }
209
210    @Override
211    public String toString() {
212        return toJsonValue().toString();
213    }
214
215    private <T extends Context> T asContext0(final Class<T> clazz) {
216        try {
217            for (Context context = this; context != null; context = context.getParent()) {
218                final Class<?> contextClass = context.getClass();
219                if (clazz.isAssignableFrom(contextClass)) {
220                    return contextClass.asSubclass(clazz).cast(context);
221                }
222            }
223            return null;
224        } catch (final Exception e) {
225            throw new IllegalArgumentException(
226                    "Unable to instantiate Context implementation class '" + clazz.getName() + "'", e);
227        }
228    }
229
230    private Context getContext0(final String contextName) {
231        for (Context context = this; context != null; context = context.getParent()) {
232            if (context.getContextName().equals(contextName)) {
233                return context;
234            }
235        }
236        return null;
237    }
238
239}