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        try {
162            maxDepth = DepthValues.valueOf(ctx.getParameter("depth")).getDepth();
163        } catch (IllegalArgumentException | NullPointerException e) {
164            maxDepth = DepthValues.root.getDepth();
165        }
166        if (value == null) {
167            value = 0;
168        }
169        value++;
170        if (value > maxDepth) {
171            throw new MaxDepthReachedException();
172        }
173        entries.put(depthKey.toLowerCase(), value);
174        return this;
175    }
176
177    /**
178     * Provides a flatten map of wrapped contexts. If a same entity type is stored in multiple contexts, the nearest one
179     * will be returned.
180     *
181     * @since 7.2
182     */
183    public final Map<String, Object> flatten() {
184        Map<String, Object> mergedResult = new HashMap<String, Object>();
185        if (parent != null) {
186            mergedResult.putAll(parent.flatten());
187        }
188        mergedResult.putAll(entries);
189        return mergedResult;
190    }
191
192    /**
193     * Gets the nearest value stored in the {@link WrappedContext}.
194     *
195     * @param ctx The {@link RenderingContext} in which the value will be searched.
196     * @param key The key used to store the value in the context.
197     * @return The casted entity.
198     * @since 7.2
199     */
200    static <T> T getEntity(RenderingContext ctx, String key) {
201        T value = null;
202        WrappedContext wrappedCtx = get(ctx);
203        if (wrappedCtx != null) {
204            if (StringUtils.isEmpty(key)) {
205                return null;
206            }
207            String realKey = key.toLowerCase().trim();
208            return wrappedCtx.innerGetEntity(realKey);
209        }
210        return value;
211    }
212
213    /**
214     * Recursive search for the nearest entity.
215     *
216     * @since 7.2
217     */
218    private final <T> T innerGetEntity(String entityType) {
219        @SuppressWarnings("unchecked")
220        T value = (T) entries.get(entityType);
221        if (value == null && parent != null) {
222            return parent.innerGetEntity(entityType);
223        }
224        return value;
225    }
226
227    /**
228     * Open the context and make all embedded entities available. Returns a {@link Closeable} which must be closed at
229     * the end.
230     * <p>
231     * Note the same context could be opened and closed several times.
232     * </p>
233     *
234     * @return A {@link Closeable} instance.
235     * @since 7.2
236     */
237    public final Closeable open() {
238        ctx.setParameterValues(WRAPPED_CONTEXT, this);
239        return new Closeable() {
240            @Override
241            public void close() throws IOException {
242                ctx.setParameterValues(WRAPPED_CONTEXT, parent);
243            }
244        };
245    }
246
247    /**
248     * Prints this context.
249     */
250    @Override
251    public String toString() {
252        return flatten().toString();
253    }
254
255}