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