001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
016 */
017
018package org.nuxeo.ecm.core.io.registry.context;
019
020import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.DEPTH_CONTROL_KEY_PREFIX;
021import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.WRAPPED_CONTEXT;
022
023import java.io.Closeable;
024import java.io.IOException;
025import java.util.HashMap;
026import java.util.Map;
027
028import org.apache.commons.lang.StringUtils;
029import org.nuxeo.ecm.core.io.registry.MarshallingException;
030
031/**
032 * Provides a way to create sub contexts of {@link RenderingContext} to broadcast marshalled entities or state
033 * parameters from a marshaller to other marshaller.
034 * <p>
035 * First, create the context, fill it and then use a try-resource statement to ensure the context will be closed.
036 * </p>
037 *
038 * <pre>
039 * <code>
040 * DocumentModel doc = ...;
041 * RenderingContext ctx = ...;
042 * try (Closeable ctx = ctx.wrap().with(ENTITY_DOCUMENT, doc).open()) {
043 *   // the document will only be available in the following statements
044 *   // call other marshallers here and put this code the get the doc : DocumentModel contextualDocument = ctx.getParameter(ENTITY_DOCUMENT);
045 * }
046 * </code>
047 * </pre>
048 * <p>
049 * Note that if in the try-resource statement, another context is created, the entity will be searched first in this
050 * context, if not found, recursively in the parent context: the nearest will be returned.
051 * </p>
052 *
053 * @since 7.2
054 */
055public final class WrappedContext {
056
057    private WrappedContext parent;
058
059    private RenderingContext ctx;
060
061    private Map<String, Object> entries = new HashMap<String, Object>();
062
063    private WrappedContext(RenderingContext ctx) {
064        if (ctx == null) {
065            throw new MarshallingException("Cannot get a wrapped context without RenderingContext");
066        }
067        this.ctx = ctx;
068        parent = ctx.getParameter(WRAPPED_CONTEXT);
069    }
070
071    private static WrappedContext get(RenderingContext ctx) {
072        if (ctx != null) {
073            return ctx.getParameter(WRAPPED_CONTEXT);
074        } else {
075            throw new MarshallingException("Cannot get a wrapped context without RenderingContext");
076        }
077    }
078
079    /**
080     * Creates a new WrappedContext in the given {@link RenderingContext}.
081     *
082     * @param ctx The {@link RenderingContext} where this {@link WrappedContext} will be available.
083     * @return The created {@link WrappedContext}.
084     * @since 7.2
085     */
086    static WrappedContext create(RenderingContext ctx) {
087        if (ctx != null) {
088            WrappedContext child = new WrappedContext(ctx);
089            return child;
090        } else {
091            throw new MarshallingException("Cannot get a wrapped context without RenderingContext");
092        }
093    }
094
095    /**
096     * Push a value in this the context.
097     *
098     * @param key The string used to get the entity.
099     * @param value The value to push.
100     * @return this {@link WrappedContext}.
101     * @since 7.2
102     */
103    public final WrappedContext with(String key, Object value) {
104        if (StringUtils.isEmpty(key)) {
105            return this;
106        }
107        String realKey = key.toLowerCase().trim();
108        entries.put(realKey, value);
109        return this;
110    }
111
112    /**
113     * Call this method to avoid an infinite loop while calling a marshaller from another.
114     * <p>
115     * This method increases the current number of "marshaller-to-marshaller" calls. And then checks that this number do
116     * not exceed the "depth" parameter. If the "depth" parameter is not provided or if it's not valid, the default
117     * value is "root" (expected valid values are "root", "children" or "max" - see {@link DepthValues}).
118     * </p>
119     * <p>
120     * Here is the prettiest way to write it:
121     *
122     * <pre>
123     * // This will control infinite loop in this marshaller
124     * try (Closeable resource = ctx.wrap().controlDepth().open()) {
125     *     // call another marshaller to fetch the desired property here
126     * } catch (MaxDepthReachedException e) {
127     *     // do not call the other marshaller
128     * }
129     * </pre>
130     *
131     * </p>
132     * <p>
133     * You can also control the depth before (usefull for list):
134     *
135     * <pre>
136     * try {
137     *     WrappedContext wrappedCtx = ctx.wrap().controlDepth();
138     *     // prepare your calls
139     *     ...
140     *     // This will control infinite loop in this marshaller
141     *     try (Closeable resource = wrappedCtx.open()) {
142     *         // call another marshaller to fetch the desired property here
143     *     }
144     * } catch (MaxDepthReachedException e) {
145     *     // manage the case
146     * }
147     * </pre>
148     *
149     * </p>
150     *
151     * @return
152     * @throws MaxDepthReachedException
153     * @since TODO
154     */
155    public final WrappedContext controlDepth() throws MaxDepthReachedException {
156        String depthKey = DEPTH_CONTROL_KEY_PREFIX + "DEFAULT";
157        Integer value = getEntity(ctx, depthKey);
158        Integer maxDepth;
159        try {
160            maxDepth = DepthValues.valueOf(ctx.getParameter("depth")).getDepth();
161        } catch (IllegalArgumentException | NullPointerException e) {
162            maxDepth = DepthValues.root.getDepth();
163        }
164        if (value == null) {
165            value = 0;
166        }
167        value++;
168        if (value > maxDepth) {
169            throw new MaxDepthReachedException();
170        }
171        entries.put(depthKey.toLowerCase(), value);
172        return this;
173    }
174
175    /**
176     * Provides a flatten map of wrapped contexts. If a same entity type is stored in multiple contexts, the nearest one
177     * will be returned.
178     *
179     * @since 7.2
180     */
181    public final Map<String, Object> flatten() {
182        Map<String, Object> mergedResult = new HashMap<String, Object>();
183        if (parent != null) {
184            mergedResult.putAll(parent.flatten());
185        }
186        mergedResult.putAll(entries);
187        return mergedResult;
188    }
189
190    /**
191     * Gets the nearest value stored in the {@link WrappedContext}.
192     *
193     * @param ctx The {@link RenderingContext} in which the value will be searched.
194     * @param key The key used to store the value in the context.
195     * @return The casted entity.
196     * @since 7.2
197     */
198    static <T> T getEntity(RenderingContext ctx, String key) {
199        T value = null;
200        WrappedContext wrappedCtx = get(ctx);
201        if (wrappedCtx != null) {
202            if (StringUtils.isEmpty(key)) {
203                return null;
204            }
205            String realKey = key.toLowerCase().trim();
206            return wrappedCtx.innerGetEntity(realKey);
207        }
208        return value;
209    }
210
211    /**
212     * Recursive search for the nearest entity.
213     *
214     * @since 7.2
215     */
216    private final <T> T innerGetEntity(String entityType) {
217        @SuppressWarnings("unchecked")
218        T value = (T) entries.get(entityType);
219        if (value == null && parent != null) {
220            return parent.innerGetEntity(entityType);
221        }
222        return value;
223    }
224
225    /**
226     * Open the context and make all embedded entities available. Returns a {@link Closeable} which must be closed at
227     * the end.
228     * <p>
229     * Note the same context could be opened and closed several times.
230     * </p>
231     *
232     * @return A {@link Closeable} instance.
233     * @since 7.2
234     */
235    public final Closeable open() {
236        ctx.setParameterValues(WRAPPED_CONTEXT, this);
237        return new Closeable() {
238            @Override
239            public void close() throws IOException {
240                ctx.setParameterValues(WRAPPED_CONTEXT, parent);
241            }
242        };
243    }
244
245    /**
246     * Prints this context.
247     */
248    @Override
249    public String toString() {
250        return flatten().toString();
251    }
252
253}