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