001/*
002 * (C) Copyright 2015-2018 Nuxeo (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.lang3.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<>();
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     * </p>
133     * <p>
134     * You can also control the depth before (usefull for list):
135     *
136     * <pre>
137     * try {
138     *     WrappedContext wrappedCtx = ctx.wrap().controlDepth();
139     *     // prepare your calls
140     *     ...
141     *     // This will control infinite loop in this marshaller
142     *     try (Closeable resource = wrappedCtx.open()) {
143     *         // call another marshaller to fetch the desired property here
144     *     }
145     * } catch (MaxDepthReachedException e) {
146     *     // manage the case
147     * }
148     * </pre>
149     * </p>
150     *
151     * @since 7.2
152     */
153    public final WrappedContext controlDepth() throws MaxDepthReachedException {
154        String depthKey = DEPTH_CONTROL_KEY_PREFIX + "DEFAULT";
155        Integer value = getEntity(ctx, depthKey);
156        Integer maxDepth;
157        String depth = ctx.getParameter("depth");
158        if (depth == null) {
159            maxDepth = DepthValues.root.getDepth();
160        } else {
161            try {
162                maxDepth = DepthValues.valueOf(depth).getDepth();
163            } catch (IllegalArgumentException | NullPointerException e) {
164                maxDepth = DepthValues.root.getDepth();
165            }
166        }
167        if (value == null) {
168            value = 0;
169        }
170        value++;
171        if (value > maxDepth) {
172            throw new MaxDepthReachedException();
173        }
174        entries.put(depthKey.toLowerCase(), value);
175        return this;
176    }
177
178    /**
179     * Provides a flatten map of wrapped contexts. If a same entity type is stored in multiple contexts, the nearest one
180     * will be returned.
181     *
182     * @since 7.2
183     */
184    public final Map<String, Object> flatten() {
185        Map<String, Object> mergedResult = new HashMap<>();
186        if (parent != null) {
187            mergedResult.putAll(parent.flatten());
188        }
189        mergedResult.putAll(entries);
190        return mergedResult;
191    }
192
193    /**
194     * Gets the nearest value stored in the {@link WrappedContext}.
195     *
196     * @param ctx The {@link RenderingContext} in which the value will be searched.
197     * @param key The key used to store the value in the context.
198     * @return The casted entity.
199     * @since 7.2
200     */
201    static <T> T getEntity(RenderingContext ctx, String key) {
202        T value = null;
203        WrappedContext wrappedCtx = get(ctx);
204        if (wrappedCtx != null) {
205            if (StringUtils.isEmpty(key)) {
206                return null;
207            }
208            String realKey = key.toLowerCase().trim();
209            return wrappedCtx.innerGetEntity(realKey);
210        }
211        return value;
212    }
213
214    /**
215     * Recursive search for the nearest entity.
216     *
217     * @since 7.2
218     */
219    private <T> T innerGetEntity(String entityType) {
220        @SuppressWarnings("unchecked")
221        T value = (T) entries.get(entityType);
222        if (value == null && parent != null) {
223            return parent.innerGetEntity(entityType);
224        }
225        return value;
226    }
227
228    /**
229     * Open the context and make all embedded entities available. Returns a {@link Closeable} which must be closed at
230     * the end.
231     * <p>
232     * Note the same context could be opened and closed several times.
233     * </p>
234     *
235     * @return A {@link Closeable} instance.
236     * @since 7.2
237     */
238    public final Closeable open() {
239        ctx.setParameterValues(WRAPPED_CONTEXT, this);
240        return new Closeable() {
241            @Override
242            public void close() throws IOException {
243                ctx.setParameterValues(WRAPPED_CONTEXT, parent);
244            }
245        };
246    }
247
248    /**
249     * Prints this context.
250     */
251    @Override
252    public String toString() {
253        return flatten().toString();
254    }
255
256}