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