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.heap;
019
020// TODO: consider detecting cyclic dependencies
021
022import static java.lang.String.format;
023import static java.util.Arrays.asList;
024import static java.util.Collections.emptyList;
025import static java.util.Collections.emptyMap;
026import static org.forgerock.json.JsonValue.json;
027import static org.forgerock.openig.decoration.global.GlobalDecorator.GLOBAL_DECORATOR_HEAP_KEY;
028import static org.forgerock.openig.heap.Keys.LOGSINK_HEAP_KEY;
029import static org.forgerock.openig.util.JsonValues.asClass;
030import static org.forgerock.openig.util.JsonValues.getWithDeprecation;
031import static org.forgerock.util.Reject.checkNotNull;
032
033import java.util.ArrayDeque;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Deque;
037import java.util.HashMap;
038import java.util.LinkedHashMap;
039import java.util.LinkedList;
040import java.util.List;
041import java.util.Map;
042import java.util.Map.Entry;
043
044import org.forgerock.http.Handler;
045import org.forgerock.http.util.MultiValueMap;
046import org.forgerock.json.JsonValue;
047import org.forgerock.json.JsonValueException;
048import org.forgerock.openig.decoration.Context;
049import org.forgerock.openig.decoration.Decorator;
050import org.forgerock.openig.decoration.global.GlobalDecorator;
051import org.forgerock.openig.log.LogSink;
052import org.forgerock.openig.log.Logger;
053
054/**
055 * The concrete implementation of a heap. Provides methods to initialize and destroy a heap.
056 * A Heap can be part of a heap hierarchy: if the queried object is not found locally, and if it has a parent,
057 * the parent will be queried (and this, recursively until there is no parent anymore).
058 */
059public class HeapImpl implements Heap {
060
061    /**
062     * List of attributes that should be ignored when looking for decorator's configuration.
063     */
064    private static final List<String> EXCLUDED_ATTRIBUTES = asList("type", "name", "config");
065
066    /**
067     * Parent heap to delegate queries to if nothing is found in the local heap.
068     * It may be null if this is the root heap (built by the system).
069     */
070    private final HeapImpl parent;
071
072    /**
073     * Heap name.
074     */
075    private final Name name;
076
077    /** Heaplets mapped to heaplet identifiers in the heap configuration. */
078    private Map<String, Heaplet> heaplets = new HashMap<>();
079
080    /** Configuration objects for heaplets. */
081    private Map<String, JsonValue> configs = new HashMap<>();
082
083    /** Objects allocated in the heap mapped to heaplet names. */
084    private Map<String, Object> objects = new HashMap<>();
085
086    /** Per-heaplet decoration contexts mapped to heaplet names. */
087    private Map<String, Context> contexts = new HashMap<>();
088
089    /** Per-heaplet decoration(s) mapped to heaplet names. */
090    private MultiValueMap<String, JsonValue> decorations =
091            new MultiValueMap<>(new LinkedHashMap<String, List<JsonValue>>());
092
093    /**
094     * Decorator for the 'main handler' reference.
095     */
096    private Decorator topLevelHandlerDecorator;
097
098    /**
099     * Top-level 'handler' reference decorations (effectively the root node of configuration).
100     */
101    private JsonValue config;
102
103    /**
104     * Heap logger.
105     */
106    private Logger logger;
107
108    /**
109     * Keep track of objects being resolved, used to avoid recursive issues.
110     */
111    private Deque<String> resolving = new ArrayDeque<>();
112
113    /**
114     * List of default object declarations to be inserted in this heap if no user-provided objects were found.
115     */
116    private List<JsonValue> defaults = new ArrayList<>();
117
118    /**
119     * Builds an anonymous root heap (will be referenced by children but has no parent itself).
120     * Intended for tests only.
121     */
122    HeapImpl() {
123        this((HeapImpl) null);
124    }
125
126    /**
127     * Builds a new anonymous heap that is a child of the given heap.
128     * Intended for tests only.
129     *
130     * @param parent
131     *         parent heap.
132     */
133    HeapImpl(final HeapImpl parent) {
134        this(parent, Name.of("anonymous"));
135    }
136
137    /**
138     * Builds a root heap (will be referenced by children but has no parent itself).
139     *
140     * @param name
141     *         local name of this heap
142     */
143    public HeapImpl(final Name name) {
144        this(null, name);
145    }
146
147    /**
148     * Builds a new heap that is a child of the given heap.
149     *
150     * @param parent
151     *         parent heap.
152     * @param name
153     *         local name of this heap
154     */
155    public HeapImpl(final HeapImpl parent, final Name name) {
156        this.parent = parent;
157        this.name = checkNotNull(name);
158    }
159
160    /**
161     * Initializes the heap using the given configuration. Once complete, all heaplets will
162     * be loaded and all associated objects are allocated using each heaplet instance's
163     * configuration.
164     *
165     * @param config the configuration root.
166     * @param reservedFieldNames the names of reserved top level fields in the config which
167     *                           should not be parsed as global decorators.
168     * @throws HeapException if an exception occurs allocating heaplets.
169     * @throws JsonValueException if the configuration object is malformed.
170     */
171    public synchronized void init(JsonValue config, String... reservedFieldNames)
172            throws HeapException {
173        // process configuration object model structure
174        this.config = config;
175        boolean logDeprecationWarning = false;
176        JsonValue heap = config.get("heap").defaultTo(emptyList());
177        if (heap.isMap()) {
178            /*
179             * In OpenIG < 3.1 the heap objects were listed in a child "objects"
180             * array. The extra nesting was found to be redundant and removed in
181             * 3.1. We continue to allow it in order to maintain backwards
182             * compatibility.
183             */
184            heap = heap.get("objects").required();
185
186            // We cannot log anything just yet because the heap is not initialized.
187            logDeprecationWarning = true;
188        }
189        for (JsonValue object : heap.expect(List.class)) {
190            addDeclaration(object);
191        }
192
193        // Register the default objects if the user has not overridden them
194        // Notice this will only verify objects declared in the heap, not the inline declarations
195        for (JsonValue value : defaults) {
196            String name = value.get("name").required().asString();
197            if (heaplets.get(name) == null) {
198                addDeclaration(value);
199            }
200            // otherwise the object configuration has been overridden
201        }
202
203        // register global decorators, ensuring that reserved field names are filtered out
204        int sz = reservedFieldNames.length;
205        String[] allReservedFieldNames = Arrays.copyOf(reservedFieldNames, sz + 1);
206        allReservedFieldNames[sz] = "heap";
207        topLevelHandlerDecorator = new GlobalDecorator(null, config, allReservedFieldNames);
208
209        if (config.isDefined("globalDecorators")) {
210            Decorator parentGlobalDecorator =
211                    parent != null ? parent.get(GLOBAL_DECORATOR_HEAP_KEY, Decorator.class) : null;
212            put(GLOBAL_DECORATOR_HEAP_KEY,
213                new GlobalDecorator(parentGlobalDecorator, config.get("globalDecorators").expect(Map.class)));
214        }
215
216        // instantiate all objects, recursively allocating dependencies
217        for (String name : new ArrayList<>(heaplets.keySet())) {
218            get(name, Object.class);
219        }
220
221        // We can log a warning now that the heap is initialized.
222        logger = new Logger(resolve(config.get("logSink").defaultTo(LOGSINK_HEAP_KEY),
223                                           LogSink.class,
224                                           true), name);
225        if (logDeprecationWarning) {
226            logger.warning("The configuration field heap/objects has been deprecated. Heap objects "
227                                   + "should now be listed directly in the top level \"heap\" field, "
228                                   + "e.g. { \"heap\" : [ objects... ] }.");
229        }
230    }
231
232    /**
233     * Add a default JsonValue object declaration in this heap.
234     *
235     * This method should only be called prior to {@link #init(JsonValue, String...)}: values provided after init() has
236     * been called will be ignored.
237     *
238     * Notice that if the caller add 2 declarations with the same name, that will ends up with an exception thrown
239     * when heap will be initialized.
240     *
241     * @param object
242     *         Object declaration (has to contains a {@literal name} attribute)
243     */
244    public void addDefaultDeclaration(final JsonValue object) {
245        defaults.add(object);
246    }
247
248    /**
249     * Add the given JsonValue as a new object declaration in this heap. The given object must be a valid object
250     * declaration ({@literal name}, {@literal type} and {@literal config} attributes). If not, a JsonValueException
251     * will be thrown. After this method is called, a new object is available in the heap.
252     *
253     * <p>The {@literal config} attribute is optional and accordingly, if empty or null, its declaration can be omitted.
254     *
255     * @param object
256     *         object declaration to add to the heap.
257     */
258    private void addDeclaration(final JsonValue object) {
259        object.required().expect(Map.class);
260        Heaplet heaplet = Heaplets.getHeaplet(asClass(object.get("type").required()));
261        if (heaplet == null) {
262            throw new JsonValueException(object.get("type"), "no heaplet available to initialize object");
263        }
264        // objects[n].name (string)
265        String name = object.get("name").required().asString();
266        if (heaplets.get(name) != null) {
267            throw new JsonValueException(object.get("name"), "object already defined");
268        }
269        // remove pre-allocated objects to be replaced
270        objects.remove(name);
271        heaplets.put(name, heaplet);
272        // objects[n].config (object)
273        configs.put(name, object.get("config").defaultTo(emptyMap()).expect(Map.class));
274        // Store decorations
275        for (JsonValue candidate : object) {
276            // Exclude standard declaration elements
277            if (!EXCLUDED_ATTRIBUTES.contains(candidate.getPointer().leaf())) {
278                decorations.add(name, candidate);
279            }
280        }
281    }
282
283    @Override
284    public <T> List<T> getAll(final Class<T> type) throws HeapException {
285        final Map<String, T> result = new HashMap<>();
286        getAll(type, result);
287        return new LinkedList<>(result.values());
288    }
289
290    private <T> void getAll(final Class<T> type,
291                            final Map<String, T> result) throws HeapException {
292        if (objects != null) {
293            for (final Entry<String, Object> entry : objects.entrySet()) {
294                if (type.isInstance(entry.getValue()) && !result.containsKey(entry.getKey())) {
295                    result.put(entry.getKey(), get(entry.getKey(), type));
296                }
297            }
298        }
299        if (parent != null) {
300            parent.getAll(type, result);
301        }
302    }
303
304    @Override
305    public <T> T get(final String name, final Class<T> type) throws HeapException {
306        return get(name, type, true);
307    }
308
309    private <T> T get(final String name, final Class<T> type, final boolean parentLookup) throws HeapException {
310        ExtractedObject extracted = extract(name, parentLookup);
311        if (extracted.object == null) {
312            return null;
313        }
314        Object o = applyGlobalDecorations(extracted);
315        return type.cast(o);
316    }
317
318    /**
319     * Extract the given named heap object from this heap or its parent (if any).
320     * The returned pointer is never {@code null} but can contains {@code null} {@literal object} and {@literal
321     * context}.
322     * The {@literal object} reference has only be decorated with it's locally (per-heaplet) defined decorations.
323     * This is the responsibility of the initial 'requester' heap (the one that have its {@link #get(String, Class)}
324     * method called in the first place) to complete the decoration with global decorators.
325     *
326     * @param name heap object name
327     * @return an {@link ExtractedObject} pair-like structure that may contains both the heap object and its
328     * associated context (never returns {@code null})
329     * @throws HeapException if extraction failed
330     */
331    ExtractedObject extract(final String name) throws HeapException {
332        return extract(name, true);
333    }
334
335    /**
336     * Extract the given named heap object from this heap or its parent (if any, and {@literal parentLookup} is true).
337     * The returned pointer is never {@code null} but can contains {@code null} {@literal object} and {@literal
338     * context}.
339     * The {@literal object} reference has only be decorated with it's locally (per-heaplet) defined decorations.
340     * This is the responsibility of the initial 'requester' heap (the one that have its {@link #get(String, Class)}
341     * method called in the first place) to complete the decoration with global decorators.
342     *
343     * @param name heap object name
344     * @param parentLookup shall we try to lookup into the parent's heap or not ?
345     * @return an {@link ExtractedObject} pair-like structure that may contains both the heap object and its
346     * associated context (never returns {@code null})
347     * @throws HeapException if extraction failed
348     */
349    ExtractedObject extract(final String name, final boolean parentLookup) throws HeapException {
350        if (resolving.contains(name)) {
351            // Fail for recursive object resolution
352            throw new HeapException(
353                    format("Object '%s' is already being constructed (probably a duplicate name in configuration)",
354                           name));
355        }
356        Object object = objects.get(name);
357        if (object == null) {
358            Heaplet heaplet = heaplets.get(name);
359            if (heaplet != null) {
360                try {
361                    resolving.push(name);
362                    JsonValue configuration = configs.get(name);
363                    object = heaplet.create(this.name.child(name), configuration, this);
364                    if (object == null) {
365                        throw new HeapException(new NullPointerException());
366                    }
367                    object = applyObjectLevelDecorations(name, object, configuration);
368                    put(name, object);
369                } finally {
370                    resolving.pop();
371                }
372            } else if (parentLookup && parent != null) {
373                // no heaplet available, query parent (if any)
374                return parent.extract(name);
375            }
376        }
377        return new ExtractedObject(object, contexts.get(name));
378    }
379
380    @Override
381    public <T> T resolve(final JsonValue reference, final Class<T> type) throws HeapException {
382        return resolve(reference, type, false);
383    }
384
385    @Override
386    public <T> T resolve(final JsonValue reference, final Class<T> type, final boolean optional) throws HeapException {
387
388        // If optional if set, accept that the provided reference may wrap a null
389        if (optional && reference.isNull()) {
390            return null;
391        }
392
393        // Otherwise we require a value
394        JsonValue required = reference.required();
395        if (required.isString()) {
396            // handle named reference; lookup into the parent's heap if not found in the local one
397            T value = get(required.asString(), type, true);
398            if (value == null) {
399                throw new JsonValueException(reference, "Object " + reference.asString() + " not found in heap");
400            }
401            return value;
402        } else if (required.isMap()) {
403            // handle inline declaration
404            String generated = name(required);
405
406            // when resolve() is called multiple times with the same reference, this prevent "already registered" errors
407            // do not lookup into parent's heap : if an object is declared with the same name into the parent's heap, it
408            // must not have the priority over the one we are currently declaring.
409            T value = get(generated, type, false);
410            if (value == null) {
411                // First resolution
412                required.put("name", generated);
413                addDeclaration(required);
414                // Get decorated object
415                value = get(generated, type, false);
416                if (value == null) {
417                    // Very unlikely to happen
418                    throw new JsonValueException(reference, "Reference is not a valid heap object");
419                }
420            }
421            return value;
422        }
423        throw new JsonValueException(reference,
424                                     format("JsonValue[%s] is neither a String nor a Map (inline declaration)",
425                                            reference.getPointer()));
426    }
427
428    /**
429     * Infer a locally unique name for the given object declaration.
430     * If a {@literal name} attribute is provided, simply return its value as name, otherwise compose a
431     * unique name composed of both the declaration JSON pointer (map to the location within the JSON file) and
432     * the value of the {@literal type} attribute (ease to identify the object).
433     * <p>
434     * The following declaration would return {@literal Inline}:
435     * <pre>
436     *     {@code
437     *     {
438     *         "name": "Inline",
439     *         "type": "Router"
440     *     }
441     *     }
442     * </pre>
443     * <p>
444     * And this one would return {@literal {WelcomeHandler}/heap/objects/0/config/defaultHandler}:
445     * <pre>
446     *     {@code
447     *     {
448     *         "type": "WelcomeHandler"
449     *     }
450     *     }
451     * </pre>
452     * @param declaration source inline object declaration
453     * @return a locally unique name
454     */
455    public static String name(final JsonValue declaration) {
456        JsonValue node = declaration.get("name");
457        if (node.isNull()) {
458            String location = declaration.getPointer().toString();
459            String type = declaration.get("type").required().asString();
460            return format("{%s}%s", type, location);
461        }
462        return node.asString();
463    }
464
465    /**
466     * Puts an object into the heap. If an object already exists in the heap with the
467     * specified name, it is overwritten.
468     *
469     * @param name name of the object to be put into the heap.
470     * @param object the object to be put into the heap.
471     */
472    public synchronized void put(final String name, final Object object) {
473        objects.put(name, object);
474        contexts.put(name, new DecorationContext(this, this.name.child(name), json(emptyMap())));
475    }
476
477    /**
478     * Decorates the given extracted heap object with global decorators.
479     *
480     * @return the extracted object (only decorated with its local decorations).
481     * @throws HeapException
482     *         if a decorator failed to apply
483     */
484    private Object applyGlobalDecorations(final ExtractedObject extracted) throws HeapException {
485
486        // Fast exit if there is nothing to decorate
487        if (extracted.object == null) {
488            return null;
489        }
490
491        // Starts with the original object
492        Object decorated = extracted.object;
493
494        // Avoid decorating decorators themselves
495        // Avoid StackOverFlow Exceptions because of infinite recursion
496        if (decorated instanceof Decorator) {
497            return decorated;
498        }
499
500        // Apply global decorations (may be inherited from parent heap)
501        ExtractedObject deco = extract(GLOBAL_DECORATOR_HEAP_KEY);
502        if (deco.object != null) {
503            Decorator globalDecorator = (Decorator) deco.object;
504            decorated = globalDecorator.decorate(decorated, null, extracted.context);
505        }
506
507        return decorated;
508    }
509
510    /**
511     * Decorates the given (local) heap object instance with its own decorations (decorations within the heap object
512     * declaration).
513     *
514     * @param name
515     *         name of the heap object
516     * @param object
517     *         delegate (to be decorated) instance
518     * @param configuration
519     *         heap object configuration
520     * @return the decorated instance (may be null if delegate object is null)
521     * @throws HeapException
522     *         when we cannot get a declared decorator from the heap
523     */
524    private Object applyObjectLevelDecorations(final String name,
525                                               final Object object,
526                                               final JsonValue configuration) throws HeapException {
527        Object decorated = object;
528
529        // Avoid decorating decorators themselves
530        // Avoid StackOverFlow Exceptions because of infinite recursion
531        if (decorated instanceof Decorator) {
532            return decorated;
533        }
534
535        // Create a context object for holding shared values
536        Context context = new DecorationContext(this,
537                                                this.name.child(name),
538                                                configuration);
539        // .. and save it for later use in extract()
540        contexts.put(name, context);
541
542        if (decorations.containsKey(name)) {
543            // We have decorators for this instance, try to apply them
544            for (JsonValue decoration : decorations.get(name)) {
545
546                // The element name is the decorator heap object name
547                String decoratorName = decoration.getPointer().leaf();
548                Decorator decorator = get(decoratorName, Decorator.class);
549                if (decorator != null) {
550                    // We just ignore when no named decorator is found
551                    // TODO Keep a list of intermediate objects for later use
552                    decorated = decorator.decorate(decorated, decoration, context);
553                }
554            }
555        }
556        return decorated;
557    }
558
559    /**
560     * Destroys the objects on the heap and dereferences all associated objects. This method
561     * calls the heaplet {@code destroy} method for each object in the heap to provide a
562     * chance for system resources to be freed.
563     */
564    public synchronized void destroy() {
565        // save the heaplets locally to send destroy notifications
566        Map<String, Heaplet> h = heaplets;
567        // prevent any further (inadvertent) object allocations
568        heaplets = new HashMap<>();
569        // all allocated objects are no longer in this heap
570        objects.clear();
571        // iterate through saved heaplets, notifying about destruction
572        for (String name : h.keySet()) {
573            h.get(name).destroy();
574        }
575        contexts.clear();
576
577    }
578
579    /**
580     * Returns the {@link Handler} object referenced by the {@literal handler} top-level attribute.
581     * The returned object is fully decorated (including top-level reference decorations).
582     *
583     * @return the {@link Handler} object referenced by the {@literal handler} top-level attribute
584     * @throws HeapException if object resolution failed
585     */
586    public Handler getHandler() throws HeapException {
587        JsonValue reference = getWithDeprecation(config, logger, "handler", "handlerObject");
588        Handler handler = resolve(reference, Handler.class);
589        // FIXME: how to grab the decoration context of this object (it may not origin from this heap) ?
590        DecorationContext context = new DecorationContext(this,
591                                                          name.child("top-level-handler"),
592                                                          json(emptyMap()));
593        return (Handler) topLevelHandlerDecorator.decorate(handler, null, context);
594    }
595
596    /**
597     * A simple data holder object letting decorators known about the decorated heap object.
598     */
599    private class DecorationContext implements Context {
600        private final Heap heap;
601        private final Name name;
602        private final JsonValue config;
603
604        public DecorationContext(final Heap heap,
605                                 final Name name,
606                                 final JsonValue config) {
607            this.heap = heap;
608            this.name = name;
609            this.config = config;
610        }
611
612        @Override
613        public Heap getHeap() {
614            return heap;
615        }
616
617        @Override
618        public Name getName() {
619            return name;
620        }
621
622        @Override
623        public JsonValue getConfig() {
624            return config;
625        }
626    }
627
628    /**
629     * Pair object containing both the retrieved heap object instance with its origin decoration context.
630     */
631    private static class ExtractedObject {
632        Object object;
633        Context context;
634
635        /**
636         * Builds a new pair object.
637         *
638         * @param object object instance (may be null)
639         * @param context decoration context (may be null)
640         */
641        public ExtractedObject(final Object object, final Context context) {
642            this.object = object;
643            this.context = context;
644        }
645    }
646}