View Javadoc

1   package org.cateproject.view.velocity;
2   
3   import java.beans.PropertyEditor;
4   import java.io.IOException;
5   import java.io.Writer;
6   import java.util.Arrays;
7   import java.util.List;
8   import java.util.Map;
9   
10  import org.apache.velocity.context.Context;
11  import org.apache.velocity.context.InternalContextAdapter;
12  import org.apache.velocity.exception.MethodInvocationException;
13  import org.apache.velocity.exception.ParseErrorException;
14  import org.apache.velocity.exception.ResourceNotFoundException;
15  import org.apache.velocity.runtime.parser.node.Node;
16  import org.springframework.beans.BeanWrapper;
17  import org.springframework.beans.BeansException;
18  import org.springframework.beans.InvalidPropertyException;
19  import org.springframework.beans.PropertyAccessorFactory;
20  import org.springframework.context.ApplicationContext;
21  import org.springframework.context.ApplicationContextAware;
22  import org.springframework.context.NoSuchMessageException;
23  import org.springframework.core.convert.ConversionService;
24  import org.springframework.util.StringUtils;
25  import org.springframework.validation.BindingResult;
26  import org.springframework.validation.Errors;
27  import org.springframework.validation.ObjectError;
28  import org.springframework.web.servlet.support.RequestContext;
29  import org.springframework.web.servlet.view.AbstractTemplateView;
30  import org.springframework.web.util.HtmlUtils;
31  
32  public class CateSpringBindDirective extends CateBlockDirective {
33  
34  	public CateSpringBindDirective() {
35  		super("path");
36  	}
37  
38  	@Override
39  	public String getName() {
40  		return "springBind";
41  	}
42  
43  	@Override
44  	public boolean render(InternalContextAdapter context, Writer writer,
45  			Node node) throws IOException, ResourceNotFoundException,
46  			ParseErrorException, MethodInvocationException {
47  		String path = getArgument(context, node, 0);
48  		RequestContext requestContext = (RequestContext)context.get(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE);
49  		ConversionService conversionService = (ConversionService) context.get(ApplicationContextAwareVelocityToolboxView.CONVERSION_SERVICE_ATTRIBUTE);
50  //		try {
51  			try {		
52  				context.put("status", requestContext.getBindStatus(path));
53  				node.jjtGetChild(1).render(context, writer);
54  			} catch(IndexOutOfBoundsException ioobe) {
55  				org.springframework.web.servlet.support.BindStatus status = new org.springframework.web.servlet.support.BindStatus(requestContext, path, true);
56  				context.put("status", status);
57  				node.jjtGetChild(1).render(context, writer);
58  			} catch(InvalidPropertyException   ipe) {
59  				BindStatus status = new BindStatus(requestContext, context, path, true,conversionService);
60  				context.put("status", status);
61  				node.jjtGetChild(1).render(context, writer);
62  			}
63  //		} catch(IllegalStateException  ise) {
64  //			BindStatus status = new BindStatus(requestContext, context, path, true,conversionService);
65  //			context.put("status", status);
66  //			node.jjtGetChild(1).render(context, writer);
67  //		}
68  		return true;
69  
70  	}
71  
72  	class BindStatus {
73  
74  		private final RequestContext requestContext;
75  
76  		private final String path;
77  
78  		private final boolean htmlEscape;
79  
80  		private final String expression;
81  
82  		private final Errors errors;
83  
84  		private BindingResult bindingResult;
85  
86  		private Object value;
87  
88  		private Class valueType;
89  
90  		private Object actualValue;
91  
92  		private PropertyEditor editor;
93  
94  		private List objectErrors;
95  
96  		private String[] errorCodes;
97  
98  		private String[] errorMessages;
99  
100 
101 		/**
102 		 * Create a new BindStatus instance, representing a field or object status.
103 		 * @param requestContext the current RequestContext
104 		 * @param path the bean and property path for which values and errors
105 		 * will be resolved (e.g. "customer.address.street")
106 		 * @param htmlEscape whether to HTML-escape error messages and string values
107 		 * @throws IllegalStateException if no corresponding Errors object found
108 		 */
109 		public BindStatus(RequestContext requestContext, Context context, String path, boolean htmlEscape, ConversionService conversionService)
110 				throws IllegalStateException {
111 
112 			this.requestContext = requestContext;
113 			this.path = path;
114 			this.htmlEscape = htmlEscape;
115 
116 			// determine name of the object and property
117 			String beanName = null;
118 			int dotPos = path.indexOf('.');
119 			if (dotPos == -1) {
120 				// property not set, only the object itself
121 				beanName = path;
122 				this.expression = null;
123 			}
124 			else {
125 				beanName = path.substring(0, dotPos);
126 				this.expression = path.substring(dotPos + 1);
127 			}
128 
129 			this.errors = requestContext.getErrors(beanName, false);
130 
131 			if (this.errors != null) {
132 				// Usual case: A BindingResult is available as request attribute.
133 				// Can determine error codes and messages for the given expression.
134 				// Can use a custom PropertyEditor, as registered by a form controller.
135 				if (this.expression != null) {
136 					if ("*".equals(this.expression)) {
137 						this.objectErrors = this.errors.getAllErrors();
138 					}
139 					else if (this.expression.endsWith("*")) {
140 						this.objectErrors = this.errors.getFieldErrors(this.expression);
141 					}
142 					else {
143 						this.objectErrors = this.errors.getFieldErrors(this.expression);
144 						this.value = this.errors.getFieldValue(this.expression);
145 						this.valueType = this.errors.getFieldType(this.expression);
146 						if (this.errors instanceof BindingResult) {
147 							this.bindingResult = (BindingResult) this.errors;
148 							this.actualValue = this.bindingResult.getRawFieldValue(this.expression);
149 							this.editor = this.bindingResult.findEditor(this.expression, null);
150 						}
151 					}
152 				}
153 				else {
154 					this.objectErrors = this.errors.getGlobalErrors();
155 				}
156 				initErrorCodes();
157 			}
158 
159 			else {
160 				// No BindingResult available as request attribute:
161 				// Probably forwarded directly to a form view.
162 				// Let's do the best we can: extract a plain target if appropriate.
163 				Object target = getModelObject(requestContext.getModel(),context,beanName);
164 				if (target == null) {
165 					throw new IllegalStateException("Neither BindingResult nor plain target object for bean name '" +
166 							beanName + "' available as request attribute");
167 				}
168 				if (this.expression != null && !"*".equals(this.expression) && !this.expression.endsWith("*")) {
169 					BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(target);
170 					bw.setConversionService(conversionService);
171 					this.valueType = bw.getPropertyType(this.expression);
172 					this.value = bw.getPropertyValue(this.expression);
173 				}
174 				this.errorCodes = new String[0];
175 				this.errorMessages = new String[0];
176 			}
177 
178 			if (htmlEscape && this.value instanceof String) {
179 				this.value = HtmlUtils.htmlEscape((String) this.value);
180 			}
181 		}
182 		
183 		
184 		private Object getModelObject(Map<String, Object> model, Context context, String modelName) {
185 		    if (model != null) {
186 				return model.get(modelName);
187 			} else {
188 				return context.get(modelName);
189 			}
190 		}
191 
192 		/**
193 		 * Extract the error codes from the ObjectError list.
194 		 */
195 		private void initErrorCodes() {
196 			this.errorCodes = new String[this.objectErrors.size()];
197 			for (int i = 0; i < this.objectErrors.size(); i++) {
198 				ObjectError error = (ObjectError) this.objectErrors.get(i);
199 				this.errorCodes[i] = error.getCode();
200 			}
201 		}
202 
203 		/**
204 		 * Extract the error messages from the ObjectError list.
205 		 */
206 		private void initErrorMessages() throws NoSuchMessageException {
207 			if (this.errorMessages == null) {
208 				this.errorMessages = new String[this.objectErrors.size()];
209 				for (int i = 0; i < this.objectErrors.size(); i++) {
210 					ObjectError error = (ObjectError) this.objectErrors.get(i);
211 					this.errorMessages[i] = this.requestContext.getMessage(error, this.htmlEscape);
212 				}
213 			}
214 		}
215 
216 
217 		/**
218 		 * Return the bean and property path for which values and errors
219 		 * will be resolved (e.g. "customer.address.street").
220 		 */
221 		public String getPath() {
222 			return this.path;
223 		}
224 
225 		/**
226 		 * Return a bind expression that can be used in HTML forms as input name
227 		 * for the respective field, or <code>null</code> if not field-specific.
228 		 * <p>Returns a bind path appropriate for resubmission, e.g. "address.street".
229 		 * Note that the complete bind path as required by the bind tag is
230 		 * "customer.address.street", if bound to a "customer" bean.
231 		 */
232 		public String getExpression() {
233 			return this.expression;
234 		}
235 
236 		/**
237 		 * Return the current value of the field, i.e. either the property value
238 		 * or a rejected update, or <code>null</code> if not field-specific.
239 		 * <p>This value will be an HTML-escaped String if the original value
240 		 * already was a String.
241 		 */
242 		public Object getValue() {
243 			return this.value;
244 		}
245 
246 		/**
247 		 * Get the '<code>Class</code>' type of the field. Favor this instead of
248 		 * '<code>getValue().getClass()</code>' since '<code>getValue()</code>' may
249 		 * return '<code>null</code>'.
250 		 */
251 		public Class getValueType() {
252 			return this.valueType;
253 		}
254 
255 		/**
256 		 * Return the actual value of the field, i.e. the raw property value,
257 		 * or <code>null</code> if not available.
258 		 */
259 		public Object getActualValue() {
260 			return this.actualValue;
261 		}
262 
263 		/**
264 		 * Return a suitable display value for the field, i.e. the stringified
265 		 * value if not null, and an empty string in case of a null value.
266 		 * <p>This value will be an HTML-escaped String if the original value
267 		 * was non-null: the <code>toString</code> result of the original value
268 		 * will get HTML-escaped.
269 		 */
270 		public String getDisplayValue() {
271 			if (this.value instanceof String) {
272 				return (String) this.value;
273 			}
274 			if (this.value != null) {
275 				return (this.htmlEscape ? HtmlUtils.htmlEscape(this.value.toString()) : this.value.toString());
276 			}
277 			return "";
278 		}
279 
280 		/**
281 		 * Return if this status represents a field or object error.
282 		 */
283 		public boolean isError() {
284 			return (this.errorCodes != null && this.errorCodes.length > 0);
285 		}
286 
287 		/**
288 		 * Return the error codes for the field or object, if any.
289 		 * Returns an empty array instead of null if none.
290 		 */
291 		public String[] getErrorCodes() {
292 			return this.errorCodes;
293 		}
294 
295 		/**
296 		 * Return the first error codes for the field or object, if any.
297 		 */
298 		public String getErrorCode() {
299 			return (this.errorCodes.length > 0 ? this.errorCodes[0] : "");
300 		}
301 
302 		/**
303 		 * Return the resolved error messages for the field or object,
304 		 * if any. Returns an empty array instead of null if none.
305 		 */
306 		public String[] getErrorMessages() {
307 			initErrorMessages();
308 			return this.errorMessages;
309 		}
310 
311 		/**
312 		 * Return the first error message for the field or object, if any.
313 		 */
314 		public String getErrorMessage() {
315 			initErrorMessages();
316 			return (this.errorMessages.length > 0 ? this.errorMessages[0] : "");
317 		}
318 
319 		/**
320 		 * Return an error message string, concatenating all messages
321 		 * separated by the given delimiter.
322 		 * @param delimiter separator string, e.g. ", " or "<br>"
323 		 * @return the error message string
324 		 */
325 		public String getErrorMessagesAsString(String delimiter) {
326 			initErrorMessages();
327 			return StringUtils.arrayToDelimitedString(this.errorMessages, delimiter);
328 		}
329 
330 		/**
331 		 * Return the Errors instance (typically a BindingResult) that this
332 		 * bind status is currently associated with.
333 		 * @return the current Errors instance, or <code>null</code> if none
334 		 * @see org.springframework.validation.BindingResult
335 		 */
336 		public Errors getErrors() {
337 			return this.errors;
338 		}
339 
340 		/**
341 		 * Return the PropertyEditor for the property that this bind status
342 		 * is currently bound to.
343 		 * @return the current PropertyEditor, or <code>null</code> if none
344 		 */
345 		public PropertyEditor getEditor() {
346 			return this.editor;
347 		}
348 
349 		/**
350 		 * Find a PropertyEditor for the given value class, associated with
351 		 * the property that this bound status is currently bound to.
352 		 * @param valueClass the value class that an editor is needed for
353 		 * @return the associated PropertyEditor, or <code>null</code> if none
354 		 */
355 		public PropertyEditor findEditor(Class valueClass) {
356 			return (this.bindingResult != null ? this.bindingResult.findEditor(this.expression, valueClass) : null);
357 		}
358 
359 
360 		@Override
361 		public String toString() {
362 			StringBuilder sb = new StringBuilder("BindStatus: ");
363 			sb.append("expression=[").append(this.expression).append("]; ");
364 			sb.append("value=[").append(this.value).append("]");
365 			if (isError()) {
366 				sb.append("; errorCodes=").append(Arrays.asList(this.errorCodes));
367 			}
368 			return sb.toString();
369 		}
370 	}
371 }