001/* 002 * (C) Copyright 2015-2018 Nuxeo (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.lang3.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<>(); 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 * </p> 133 * <p> 134 * You can also control the depth before (usefull for list): 135 * 136 * <pre> 137 * try { 138 * WrappedContext wrappedCtx = ctx.wrap().controlDepth(); 139 * // prepare your calls 140 * ... 141 * // This will control infinite loop in this marshaller 142 * try (Closeable resource = wrappedCtx.open()) { 143 * // call another marshaller to fetch the desired property here 144 * } 145 * } catch (MaxDepthReachedException e) { 146 * // manage the case 147 * } 148 * </pre> 149 * </p> 150 * 151 * @since 7.2 152 */ 153 public final WrappedContext controlDepth() throws MaxDepthReachedException { 154 String depthKey = DEPTH_CONTROL_KEY_PREFIX + "DEFAULT"; 155 Integer value = getEntity(ctx, depthKey); 156 Integer maxDepth; 157 String depth = ctx.getParameter("depth"); 158 if (depth == null) { 159 maxDepth = DepthValues.root.getDepth(); 160 } else { 161 try { 162 maxDepth = DepthValues.valueOf(depth).getDepth(); 163 } catch (IllegalArgumentException | NullPointerException e) { 164 maxDepth = DepthValues.root.getDepth(); 165 } 166 } 167 if (value == null) { 168 value = 0; 169 } 170 value++; 171 if (value > maxDepth) { 172 throw new MaxDepthReachedException(); 173 } 174 entries.put(depthKey.toLowerCase(), value); 175 return this; 176 } 177 178 /** 179 * Provides a flatten map of wrapped contexts. If a same entity type is stored in multiple contexts, the nearest one 180 * will be returned. 181 * 182 * @since 7.2 183 */ 184 public final Map<String, Object> flatten() { 185 Map<String, Object> mergedResult = new HashMap<>(); 186 if (parent != null) { 187 mergedResult.putAll(parent.flatten()); 188 } 189 mergedResult.putAll(entries); 190 return mergedResult; 191 } 192 193 /** 194 * Gets the nearest value stored in the {@link WrappedContext}. 195 * 196 * @param ctx The {@link RenderingContext} in which the value will be searched. 197 * @param key The key used to store the value in the context. 198 * @return The casted entity. 199 * @since 7.2 200 */ 201 static <T> T getEntity(RenderingContext ctx, String key) { 202 T value = null; 203 WrappedContext wrappedCtx = get(ctx); 204 if (wrappedCtx != null) { 205 if (StringUtils.isEmpty(key)) { 206 return null; 207 } 208 String realKey = key.toLowerCase().trim(); 209 return wrappedCtx.innerGetEntity(realKey); 210 } 211 return value; 212 } 213 214 /** 215 * Recursive search for the nearest entity. 216 * 217 * @since 7.2 218 */ 219 private <T> T innerGetEntity(String entityType) { 220 @SuppressWarnings("unchecked") 221 T value = (T) entries.get(entityType); 222 if (value == null && parent != null) { 223 return parent.innerGetEntity(entityType); 224 } 225 return value; 226 } 227 228 /** 229 * Open the context and make all embedded entities available. Returns a {@link Closeable} which must be closed at 230 * the end. 231 * <p> 232 * Note the same context could be opened and closed several times. 233 * </p> 234 * 235 * @return A {@link Closeable} instance. 236 * @since 7.2 237 */ 238 public final Closeable open() { 239 ctx.setParameterValues(WRAPPED_CONTEXT, this); 240 return new Closeable() { 241 @Override 242 public void close() throws IOException { 243 ctx.setParameterValues(WRAPPED_CONTEXT, parent); 244 } 245 }; 246 } 247 248 /** 249 * Prints this context. 250 */ 251 @Override 252 public String toString() { 253 return flatten().toString(); 254 } 255 256}