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