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}