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