/*
* (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* Nicolas Chapurlat <nchapurlat@nuxeo.com>
*/
package org.nuxeo.ecm.core.io.registry.context;
import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.DEPTH_CONTROL_KEY_PREFIX;
import static org.nuxeo.ecm.core.io.registry.MarshallingConstants.WRAPPED_CONTEXT;
import java.io.Closeable;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.nuxeo.ecm.core.io.registry.MarshallingException;
/**
* Provides a way to create sub contexts of {@link RenderingContext} to broadcast marshalled entities or state
* parameters from a marshaller to other marshaller.
* <p>
* First, create the context, fill it and then use a try-resource statement to ensure the context will be closed.
* </p>
*
* <pre>
* <code>
* DocumentModel doc = ...;
* RenderingContext ctx = ...;
* try (Closeable ctx = ctx.wrap().with(ENTITY_DOCUMENT, doc).open()) {
* // the document will only be available in the following statements
* // call other marshallers here and put this code the get the doc : DocumentModel contextualDocument = ctx.getParameter(ENTITY_DOCUMENT);
* }
* </code>
* </pre>
* <p>
* Note that if in the try-resource statement, another context is created, the entity will be searched first in this
* context, if not found, recursively in the parent context: the nearest will be returned.
* </p>
*
* @since 7.2
*/
public final class WrappedContext {
private WrappedContext parent;
private RenderingContext ctx;
private Map<String, Object> entries = new HashMap<String, Object>();
private WrappedContext(RenderingContext ctx) {
if (ctx == null) {
throw new MarshallingException("Cannot get a wrapped context without RenderingContext");
}
this.ctx = ctx;
parent = ctx.getParameter(WRAPPED_CONTEXT);
}
private static WrappedContext get(RenderingContext ctx) {
if (ctx != null) {
return ctx.getParameter(WRAPPED_CONTEXT);
} else {
throw new MarshallingException("Cannot get a wrapped context without RenderingContext");
}
}
/**
* Creates a new WrappedContext in the given {@link RenderingContext}.
*
* @param ctx The {@link RenderingContext} where this {@link WrappedContext} will be available.
* @return The created {@link WrappedContext}.
* @since 7.2
*/
static WrappedContext create(RenderingContext ctx) {
if (ctx != null) {
WrappedContext child = new WrappedContext(ctx);
return child;
} else {
throw new MarshallingException("Cannot get a wrapped context without RenderingContext");
}
}
/**
* Push a value in this the context.
*
* @param key The string used to get the entity.
* @param value The value to push.
* @return this {@link WrappedContext}.
* @since 7.2
*/
public final WrappedContext with(String key, Object value) {
if (StringUtils.isEmpty(key)) {
return this;
}
String realKey = key.toLowerCase().trim();
entries.put(realKey, value);
return this;
}
/**
* Call this method to avoid an infinite loop while calling a marshaller from another.
* <p>
* This method increases the current number of "marshaller-to-marshaller" calls. And then checks that this number do
* not exceed the "depth" parameter. If the "depth" parameter is not provided or if it's not valid, the default
* value is "root" (expected valid values are "root", "children" or "max" - see {@link DepthValues}).
* </p>
* <p>
* Here is the prettiest way to write it:
*
* <pre>
* // This will control infinite loop in this marshaller
* try (Closeable resource = ctx.wrap().controlDepth().open()) {
* // call another marshaller to fetch the desired property here
* } catch (MaxDepthReachedException e) {
* // do not call the other marshaller
* }
* </pre>
*
* </p>
* <p>
* You can also control the depth before (usefull for list):
*
* <pre>
* try {
* WrappedContext wrappedCtx = ctx.wrap().controlDepth();
* // prepare your calls
* ...
* // This will control infinite loop in this marshaller
* try (Closeable resource = wrappedCtx.open()) {
* // call another marshaller to fetch the desired property here
* }
* } catch (MaxDepthReachedException e) {
* // manage the case
* }
* </pre>
*
* </p>
*
* @return
* @throws MaxDepthReachedException
* @since TODO
*/
public final WrappedContext controlDepth() throws MaxDepthReachedException {
String depthKey = DEPTH_CONTROL_KEY_PREFIX + "DEFAULT";
Integer value = getEntity(ctx, depthKey);
Integer maxDepth;
String depth = ctx.getParameter("depth");
if (depth == null) {
maxDepth = DepthValues.root.getDepth();
} else {
try {
maxDepth = DepthValues.valueOf(depth).getDepth();
} catch (IllegalArgumentException | NullPointerException e) {
maxDepth = DepthValues.root.getDepth();
}
}
if (value == null) {
value = 0;
}
value++;
if (value > maxDepth) {
throw new MaxDepthReachedException();
}
entries.put(depthKey.toLowerCase(), value);
return this;
}
/**
* Provides a flatten map of wrapped contexts. If a same entity type is stored in multiple contexts, the nearest one
* will be returned.
*
* @since 7.2
*/
public final Map<String, Object> flatten() {
Map<String, Object> mergedResult = new HashMap<String, Object>();
if (parent != null) {
mergedResult.putAll(parent.flatten());
}
mergedResult.putAll(entries);
return mergedResult;
}
/**
* Gets the nearest value stored in the {@link WrappedContext}.
*
* @param ctx The {@link RenderingContext} in which the value will be searched.
* @param key The key used to store the value in the context.
* @return The casted entity.
* @since 7.2
*/
static <T> T getEntity(RenderingContext ctx, String key) {
T value = null;
WrappedContext wrappedCtx = get(ctx);
if (wrappedCtx != null) {
if (StringUtils.isEmpty(key)) {
return null;
}
String realKey = key.toLowerCase().trim();
return wrappedCtx.innerGetEntity(realKey);
}
return value;
}
/**
* Recursive search for the nearest entity.
*
* @since 7.2
*/
private final <T> T innerGetEntity(String entityType) {
@SuppressWarnings("unchecked")
T value = (T) entries.get(entityType);
if (value == null && parent != null) {
return parent.innerGetEntity(entityType);
}
return value;
}
/**
* Open the context and make all embedded entities available. Returns a {@link Closeable} which must be closed at
* the end.
* <p>
* Note the same context could be opened and closed several times.
* </p>
*
* @return A {@link Closeable} instance.
* @since 7.2
*/
public final Closeable open() {
ctx.setParameterValues(WRAPPED_CONTEXT, this);
return new Closeable() {
@Override
public void close() throws IOException {
ctx.setParameterValues(WRAPPED_CONTEXT, parent);
}
};
}
/**
* Prints this context.
*/
@Override
public String toString() {
return flatten().toString();
}
}