View Javadoc

1   /*
2    * Project Bimbo.
3    * Copyright 2008 Frank Cornelis.
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package net.sf.bimbo;
19  
20  import static net.sf.bimbo.impl.HtmlUtil.escape;
21  
22  import java.io.IOException;
23  import java.io.PrintWriter;
24  import java.lang.reflect.Field;
25  import java.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Method;
27  import java.lang.reflect.Modifier;
28  import java.security.Principal;
29  import java.util.Date;
30  import java.util.HashMap;
31  import java.util.LinkedList;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.UUID;
35  
36  import javax.annotation.PostConstruct;
37  import javax.annotation.Resource;
38  import javax.naming.InitialContext;
39  import javax.naming.NamingException;
40  import javax.naming.NoInitialContextException;
41  import javax.servlet.ServletConfig;
42  import javax.servlet.ServletException;
43  import javax.servlet.http.HttpServlet;
44  import javax.servlet.http.HttpServletRequest;
45  import javax.servlet.http.HttpServletResponse;
46  import javax.servlet.http.HttpSession;
47  
48  import net.sf.bimbo.impl.AccountPage;
49  import net.sf.bimbo.impl.BimboPrincipal;
50  import net.sf.bimbo.impl.BooleanRenderer;
51  import net.sf.bimbo.impl.DateRenderer;
52  import net.sf.bimbo.impl.DoubleRenderer;
53  import net.sf.bimbo.impl.FloatRenderer;
54  import net.sf.bimbo.impl.HtmlElement;
55  import net.sf.bimbo.impl.IntegerRenderer;
56  import net.sf.bimbo.impl.LoginPage;
57  import net.sf.bimbo.impl.StringRenderer;
58  import net.sf.bimbo.impl.StyleAdviceManager;
59  import net.sf.bimbo.spi.ConversionException;
60  import net.sf.bimbo.spi.Renderer;
61  
62  import org.apache.commons.logging.Log;
63  import org.apache.commons.logging.LogFactory;
64  
65  /**
66   * The Bimbo web application runtime servlet. This servlet interprets all Bimbo
67   * annotations of your web application.
68   * 
69   * <p>
70   * The required init parameter <tt>WelcomePage</tt> indicates the welcome page
71   * full class name. All other web application configuration is driven by
72   * annotations.
73   * </p>
74   * 
75   * @author fcorneli
76   * 
77   */
78  public class BimboServlet extends HttpServlet implements BimboContext {
79  
80  	private static final long serialVersionUID = 1L;
81  
82  	private static final Log LOG = LogFactory.getLog(BimboServlet.class);
83  
84  	private Class<?> welcomePageClass;
85  
86  	@Override
87  	public void init(ServletConfig config) throws ServletException {
88  		super.init(config);
89  		String welcomePageInitParameter = config
90  				.getInitParameter("WelcomePage");
91  		if (null != welcomePageInitParameter) {
92  			String welcomePageClassName = welcomePageInitParameter.trim();
93  			this.welcomePageClass = loadClass(welcomePageClassName);
94  			StyleAdviceManager.setWelcomePageClass(this.welcomePageClass);
95  		}
96  	}
97  
98  	public static Class<?> loadClass(String className) throws ServletException {
99  		Thread currentThread = Thread.currentThread();
100 		ClassLoader classLoader = currentThread.getContextClassLoader();
101 		try {
102 			return classLoader.loadClass(className);
103 		} catch (ClassNotFoundException e) {
104 			throw new ServletException("invalid page class: " + className);
105 		}
106 	}
107 
108 	@Override
109 	protected void doGet(HttpServletRequest request,
110 			HttpServletResponse response) throws ServletException, IOException {
111 		LOG.debug("doGet");
112 		PrintWriter writer = response.getWriter();
113 		if (null == this.welcomePageClass) {
114 			showErrorPage(writer);
115 		} else {
116 			Object pageObject = null;
117 			try {
118 				pageObject = createPage(this.welcomePageClass, request,
119 						response);
120 			} catch (Exception e) {
121 				LOG.debug("create page error: " + e.getMessage(), e);
122 				writer.println("Could not instantiate page class: "
123 						+ this.welcomePageClass.getName());
124 			}
125 			outputPage(this.welcomePageClass, pageObject, request, response);
126 		}
127 	}
128 
129 	private void outputPage(Class<?> pageClass, Object page,
130 			HttpServletRequest request, HttpServletResponse response)
131 			throws IOException {
132 		PrintWriter writer = response.getWriter();
133 		HttpSession session = request.getSession();
134 		outputPage(pageClass, page, writer, session);
135 		outputPage(pageClass, page, writer, session);
136 	}
137 
138 	private void showErrorPage(PrintWriter writer) {
139 		writer.println("<html>");
140 		{
141 			writer.println("<body>");
142 			{
143 				writer.println("<p>Not configured</p>");
144 			}
145 			writer.println("</body>");
146 		}
147 		writer.println("</html>");
148 	}
149 
150 	private void outputPage(Class<?> pageClass, Object page,
151 			PrintWriter writer, HttpSession session) {
152 		outputPage(pageClass, page, session, null,
153 				new HashMap<String, String>(), writer);
154 	}
155 
156 	private String webappTitle = null;
157 
158 	private void outputPage(Class<?> pageClass, Object page,
159 			HttpSession session, String message,
160 			Map<String, String> constraintViolations, PrintWriter writer) {
161 		Package pagePackage = pageClass.getPackage();
162 		if (null != pagePackage) {
163 			Title packageTitle = pagePackage.getAnnotation(Title.class);
164 			if (null != packageTitle) {
165 				/*
166 				 * We cache the webapp title in this servlet.
167 				 */
168 				this.webappTitle = packageTitle.value();
169 			}
170 		}
171 
172 		StyleAdviceManager styleAdviceManager = new StyleAdviceManager(
173 				pageClass);
174 
175 		writer.println("<html>");
176 
177 		writer.println("<head>");
178 		{
179 			if (null != this.webappTitle) {
180 				new HtmlElement("title").setBody(webappTitle).write(writer);
181 			}
182 			new HtmlElement("meta").addAttribute("name", "Identifier")
183 					.addAttribute("content", UUID.randomUUID().toString())
184 					.write(writer);
185 		}
186 		writer.println("</head>");
187 
188 		{
189 			writer.println("<body>");
190 			{
191 				outputJavascript(pageClass, writer);
192 				if (null != webappTitle) {
193 					HtmlElement h1Element = new HtmlElement("h1")
194 							.setBody(webappTitle);
195 					String style = styleAdviceManager
196 							.getApplicationTitleStyle();
197 					h1Element.addAttribute("style", style);
198 					h1Element.write(writer);
199 				}
200 				String username = (String) session
201 						.getAttribute(USERNAME_SESSION_ATTRIBUTE);
202 				String identityContent;
203 				if (null == username) {
204 					identityContent = "Welcome, Guest";
205 				} else {
206 					identityContent = "Welcome, "
207 							+ "<a href=\"javascript:doGlobalAction('account');\">"
208 							+ username
209 							+ "</a>&nbsp;"
210 							+ "<a href=\"javascript:doGlobalAction('logout');\">Logout</a>";
211 				}
212 				identityContent += "&nbsp;<a href=\"javascript:doGlobalAction('home');\">Home</a>";
213 				new HtmlElement("div")
214 						.addAttribute(
215 								"style",
216 								"position: absolute; right: 0%; text-align: right; background-color: #e0e0e0; border-style: solid; border-width: 1px; border-color: black; padding-left: 5px; padding-right: 5px;")
217 						.setEscapedBody(identityContent).write(writer);
218 				String pageTitle = getTitle(pageClass);
219 				HtmlElement h2Element = new HtmlElement("h2")
220 						.setBody(pageTitle);
221 				h2Element.addAttribute("style", styleAdviceManager
222 						.getPageTitleStyle());
223 				h2Element.write(writer);
224 
225 				writer.println("<form name=\"GlobalActionForm\" action=\""
226 						+ pageClass.getSimpleName()
227 						+ ".bimbo\" method=\"POST\">");
228 				writer
229 						.println("<input type=\"hidden\" name=\"GlobalActionName\"/>");
230 				writer.println("</form>");
231 
232 				writer.println("<form name=\"ActionForm\" action=\""
233 						+ pageClass.getSimpleName()
234 						+ ".bimbo\" method=\"POST\">");
235 				{
236 					new HtmlElement("input").addAttribute("type", "hidden")
237 							.addAttribute("name", "PageClass").addAttribute(
238 									"value", pageClass.getName()).write(writer);
239 					new HtmlElement("input").addAttribute("type", "hidden")
240 							.addAttribute("name", "ActionName").write(writer);
241 					writePageContent(pageClass, page, constraintViolations,
242 							message, writer);
243 				}
244 				writer.println("</form>");
245 			}
246 			writer.println("</body>");
247 		}
248 		writer.println("</html>");
249 	}
250 
251 	private void outputJavascript(Class<?> pageClass, PrintWriter writer) {
252 		writer.println("<script type=\"text/javascript\">");
253 		{
254 			writer.println("function doAction(name) {");
255 			writer.println("\tdocument.ActionForm.ActionName.value = name;");
256 			writer.println("\tdocument.ActionForm.submit();");
257 			writer.println("}");
258 			writer.println();
259 			writer.println("function doConfirmAction(name, confirmation) {");
260 			writer.println("\tvar answer = confirm(confirmation);");
261 			writer.println("\tif (answer) {");
262 			writer.println("\t\tdocument.ActionForm.ActionName.value = name;");
263 			writer.println("\t\tdocument.ActionForm.submit();");
264 			writer.println("\t}");
265 			writer.println("}");
266 			writer.println();
267 			writer.println("function doGlobalAction(name) {");
268 			writer
269 					.println("\tdocument.GlobalActionForm.GlobalActionName.value = name;");
270 			writer.println("\tdocument.GlobalActionForm.submit();");
271 			writer.println("}");
272 			writer.println();
273 			writer.println("var req = false;");
274 			writer.println("function doAjaxAction(name) {");
275 			{
276 				writer.println("\tif (window.XMLHttpRequest) {");
277 				{
278 					writer
279 							.println("\t\ttry {req = new XMLHttpRequest();} catch(e) {}");
280 					writer.println("\t} else {");
281 					writer.println("\t\ttry {");
282 					writer
283 							.println("\t\t\treq = new ActiveXObject('Msxml2.XMLHTTP');");
284 					writer.println("\t\t} catch(e) {");
285 					writer
286 							.println("\t\t\ttry {req = new ActiveXObject('Microsoft.XMLHTTP');} catch(e) {}");
287 					writer.println("\t\t}");
288 				}
289 				writer.println("\t}");
290 			}
291 			writer.println("\tif (req) {");
292 			writer
293 					.println("document.getElementById('bimbo.ajax.message').innerHTML = 'Processing...';");
294 			writer.println("var params='AjaxActionName=' + escape(name);");
295 			writer.println("\t\treq.onreadystatechange = processAjaxResponse;");
296 			writer.println("\t\treq.open('POST', '" + pageClass.getSimpleName()
297 					+ ".bimbo', true);");
298 			writer
299 					.println("\t\treq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');");
300 			writer
301 					.println("\t\treq.setRequestHeader('Content-Length', params.length);");
302 			writer.println("\t\treq.setRequestHeader('Connection', 'close');");
303 			writer.println("\t\treq.send(params);");
304 			writer.println("\t}");
305 			writer.println("}");
306 			writer.println();
307 			writer.println("function processAjaxResponse() {");
308 			writer.println("\tif (req.readyState == 4) {");
309 			writer.println("\t\tif (req.status == 200) {");
310 			writer
311 					.println("\t\t\tdocument.getElementById('bimbo.ajax.message').innerHTML = '';");
312 			writer.println("\t\t}");
313 			writer.println("\t}");
314 			writer.println("}");
315 		}
316 		writer.println("</script>");
317 	}
318 
319 	private interface GlobalAction {
320 		void doAction(BimboServlet bimboServlet, HttpServletRequest request,
321 				HttpServletResponse response) throws Exception;
322 	}
323 
324 	private static final Map<String, GlobalAction> globalActions = new HashMap<String, GlobalAction>();
325 
326 	static {
327 		globalActions.put("account", new ShowAccount());
328 		globalActions.put("logout", new Logout());
329 		globalActions.put("home", new ShowHome());
330 	}
331 
332 	private static class ShowAccount implements GlobalAction {
333 
334 		public void doAction(BimboServlet bimboServlet,
335 				HttpServletRequest request, HttpServletResponse response)
336 				throws Exception {
337 			AccountPage accountPage = bimboServlet.createPage(
338 					AccountPage.class, request, response);
339 			PrintWriter writer = response.getWriter();
340 			HttpSession session = request.getSession();
341 			bimboServlet.outputPage(AccountPage.class, accountPage, writer,
342 					session);
343 		}
344 	}
345 
346 	private static class Logout implements GlobalAction {
347 
348 		public void doAction(BimboServlet bimboServlet,
349 				HttpServletRequest request, HttpServletResponse response)
350 				throws Exception {
351 			HttpSession session = request.getSession();
352 			session.removeAttribute(USERNAME_SESSION_ATTRIBUTE);
353 			Object welcomePage = bimboServlet.createPage(
354 					bimboServlet.welcomePageClass, request, response);
355 			PrintWriter writer = response.getWriter();
356 			bimboServlet.outputPage(bimboServlet.welcomePageClass, welcomePage,
357 					writer, session);
358 		}
359 	}
360 
361 	private static class ShowHome implements GlobalAction {
362 
363 		public void doAction(BimboServlet bimboServlet,
364 				HttpServletRequest request, HttpServletResponse response)
365 				throws Exception {
366 			HttpSession session = request.getSession();
367 			Object welcomePage = bimboServlet.createPage(
368 					bimboServlet.welcomePageClass, request, response);
369 			PrintWriter writer = response.getWriter();
370 			bimboServlet.outputPage(bimboServlet.welcomePageClass, welcomePage,
371 					writer, session);
372 		}
373 	}
374 
375 	@Override
376 	protected void doPost(HttpServletRequest request,
377 			HttpServletResponse response) throws ServletException, IOException {
378 		LOG.debug("doPost");
379 		PrintWriter writer = response.getWriter();
380 
381 		String globalActionName = request.getParameter("GlobalActionName");
382 		if (null != globalActionName) {
383 			LOG.debug("global action: " + globalActionName);
384 			GlobalAction globalAction = globalActions.get(globalActionName);
385 			if (null == globalAction) {
386 				writer
387 						.println("unsupported global action: "
388 								+ globalActionName);
389 				return;
390 			}
391 			try {
392 				globalAction.doAction(this, request, response);
393 			} catch (Exception e) {
394 				writer.println("error executing global action: "
395 						+ globalActionName);
396 				return;
397 			}
398 			return;
399 		}
400 
401 		String ajaxActionName = request.getParameter("AjaxActionName");
402 		if (null != ajaxActionName) {
403 			LOG.debug("ajax action: " + ajaxActionName);
404 
405 			return;
406 		}
407 
408 		String pageClassName = request.getParameter("PageClass");
409 		LOG.debug("page class: " + pageClassName);
410 		if (null == pageClassName) {
411 			writer.println("need a PageClass parameter");
412 			return;
413 		}
414 		Class<?> pageClass = loadClass(pageClassName);
415 
416 		String actionName = request.getParameter("ActionName");
417 		LOG.debug("action name: " + actionName);
418 		if (null == actionName) {
419 			writer.println("need an ActionName parameter");
420 			return;
421 		}
422 		Method actionMethod;
423 		if (-1 != actionName.indexOf("(")) {
424 			LOG.debug("table action requested");
425 			String tableFieldName = actionName.substring(actionName
426 					.indexOf("(") + 1, actionName.indexOf("."));
427 			LOG.debug("table field name: " + tableFieldName);
428 			Field tableField;
429 			try {
430 				tableField = pageClass.getDeclaredField(tableFieldName);
431 			} catch (Exception e) {
432 				writer.println("field not found: " + tableFieldName);
433 				return;
434 			}
435 			if (false == List.class.equals(tableField.getType())) {
436 				writer.println("field is not a list: " + tableFieldName);
437 				return;
438 			}
439 			Integer tableActionIdx = Integer.parseInt(actionName.substring(
440 					actionName.indexOf(".") + 1, actionName.indexOf(")")));
441 			LOG.debug("table action idx: " + tableActionIdx);
442 			String tableEntryType = request.getParameter(tableFieldName + "."
443 					+ tableActionIdx);
444 			LOG.debug("table entry type: " + tableEntryType);
445 			Class<?> tableEntryClass = loadClass(tableEntryType);
446 			String actionMethodName = actionName.substring(0, actionName
447 					.indexOf("("));
448 			try {
449 				actionMethod = pageClass.getDeclaredMethod(actionMethodName,
450 						new Class[] { tableEntryClass });
451 			} catch (Exception e) {
452 				LOG.debug("no action method found for name: "
453 						+ actionMethodName);
454 				writer.println("no action method found for name: "
455 						+ actionMethodName);
456 				return;
457 			}
458 		} else {
459 			try {
460 				actionMethod = pageClass.getDeclaredMethod(actionName,
461 						new Class[] {});
462 			} catch (Exception e) {
463 				writer
464 						.println("no action method found for name: "
465 								+ actionName);
466 				return;
467 			}
468 		}
469 		Action actionAnnotation = actionMethod.getAnnotation(Action.class);
470 		if (null == actionAnnotation) {
471 			writer.println("action method not annotated with @Action: "
472 					+ actionName);
473 			return;
474 		}
475 
476 		HttpSession session = request.getSession();
477 
478 		Object page;
479 		try {
480 			page = createPage(pageClass, request, response);
481 		} catch (Exception e) {
482 			writer.println("could not init page: " + pageClassName
483 					+ ". Missing default constructor?");
484 			return;
485 		}
486 
487 		Map<String, String> conversionErrors;
488 		try {
489 			conversionErrors = restorePageFromRequest(request, pageClass, page);
490 		} catch (Exception e) {
491 			LOG.debug("error on restore: " + e.getMessage(), e);
492 			throw new ServletException("error on restore: " + e.getMessage(), e);
493 		}
494 		if (false == conversionErrors.isEmpty()) {
495 			LOG.debug("conversion errors: " + conversionErrors);
496 			outputPage(pageClass, page, session, null, conversionErrors, writer);
497 			return;
498 		}
499 
500 		if (false == actionAnnotation.skipConstraints()) {
501 			Map<String, String> constraintViolationFieldNames = checkConstraints(
502 					pageClass, page);
503 			if (false == constraintViolationFieldNames.isEmpty()) {
504 				outputPage(pageClass, page, session, null,
505 						constraintViolationFieldNames, writer);
506 				return;
507 			}
508 		}
509 
510 		Authenticated authenticatedAnnotation = actionMethod
511 				.getAnnotation(Authenticated.class);
512 		if (null != authenticatedAnnotation) {
513 			LOG.debug("authentication required");
514 			String username = (String) session
515 					.getAttribute(USERNAME_SESSION_ATTRIBUTE);
516 			if (null == username) {
517 				LOG.debug("login required");
518 				LoginPage loginPage;
519 				try {
520 					loginPage = createPage(LoginPage.class, request, response);
521 				} catch (Exception e) {
522 					writer.println("could not init login page");
523 					return;
524 				}
525 				LoginPage.saveActionPage(page, actionName, session);
526 
527 				outputPage(LoginPage.class, loginPage, writer, session);
528 				return;
529 			}
530 		}
531 
532 		Object actionParam = null;
533 		if (-1 != actionName.indexOf("(")) {
534 			LOG.debug("table action requested");
535 			String tableFieldName = actionName.substring(actionName
536 					.indexOf("(") + 1, actionName.indexOf("."));
537 			LOG.debug("table field name: " + tableFieldName);
538 			Field tableField;
539 			try {
540 				tableField = pageClass.getDeclaredField(tableFieldName);
541 			} catch (Exception e) {
542 				writer.println("field not found: " + tableFieldName);
543 				return;
544 			}
545 			if (false == List.class.equals(tableField.getType())) {
546 				writer.println("field is not a list: " + tableFieldName);
547 				return;
548 			}
549 			Integer tableActionIdx = Integer.parseInt(actionName.substring(
550 					actionName.indexOf(".") + 1, actionName.indexOf(")")));
551 			tableField.setAccessible(true);
552 			List<?> table;
553 			try {
554 				table = (List<?>) tableField.get(page);
555 			} catch (Exception e) {
556 				writer.println("error reading list: " + tableFieldName);
557 				return;
558 			}
559 			actionParam = table.get(tableActionIdx);
560 		}
561 		performAction(page, actionName, actionParam, session, writer, request,
562 				response);
563 	}
564 
565 	public void performAction(Object page, String actionName,
566 			Object actionParam, HttpSession session, PrintWriter writer,
567 			HttpServletRequest request, HttpServletResponse response)
568 			throws IOException {
569 		Class<?> pageClass = page.getClass();
570 		Method actionMethod;
571 		if (null != actionParam) {
572 			Class<?> actionParamType = actionParam.getClass();
573 			try {
574 				actionMethod = pageClass.getDeclaredMethod(actionName
575 						.substring(0, actionName.indexOf("(")),
576 						new Class[] { actionParamType });
577 			} catch (Exception e) {
578 				LOG.debug("no action method found for name: " + actionName
579 						+ " and param type " + actionParamType.getName());
580 				writer.println("no action method found for name: " + actionName
581 						+ " and param type " + actionParamType.getName());
582 				return;
583 			}
584 		} else {
585 			try {
586 				actionMethod = pageClass.getDeclaredMethod(actionName,
587 						new Class[] {});
588 			} catch (Exception e) {
589 				writer
590 						.println("no action method found for name: "
591 								+ actionName);
592 				return;
593 			}
594 		}
595 		Action actionAnnotation = actionMethod.getAnnotation(Action.class);
596 		if (null == actionAnnotation) {
597 			writer.println("action method not annotated with @Action: "
598 					+ actionName);
599 			return;
600 		}
601 		Object resultPage;
602 		try {
603 			if (null == actionParam) {
604 				resultPage = actionMethod.invoke(page, new Object[] {});
605 			} else {
606 				resultPage = actionMethod.invoke(page,
607 						new Object[] { actionParam });
608 			}
609 		} catch (InvocationTargetException e) {
610 			String actionLabel;
611 			if ("".equals(actionAnnotation.value())) {
612 				actionLabel = actionMethod.getName();
613 			} else {
614 				actionLabel = actionAnnotation.value();
615 			}
616 			Throwable targetException = e.getTargetException();
617 			Map<String, String> constraintMessages = blameInputFields(page,
618 					targetException);
619 			LOG.debug("# blamed fields: " + constraintMessages.size());
620 			String message;
621 			if (constraintMessages.isEmpty()) {
622 				message = actionLabel + " Error: "
623 						+ targetException.getMessage();
624 				LOG.debug("error message: " + message);
625 			} else {
626 				LOG.debug("contraint messages: " + constraintMessages);
627 				message = null;
628 			}
629 			outputPage(pageClass, page, session, message, constraintMessages,
630 					writer);
631 			return;
632 		} catch (Exception e) {
633 			LOG.debug("error invoking action: " + e.getMessage());
634 			LOG.debug("exception type: " + e.getClass().getName());
635 			writer.println("error invoking action: " + actionName);
636 			return;
637 		} finally {
638 			saveSessionAttribute(pageClass, page, session);
639 		}
640 		if (null == resultPage) {
641 			/*
642 			 * This means that the page did it's own output.
643 			 */
644 			return;
645 		}
646 
647 		Class<?> resultPageClass = resultPage.getClass();
648 		try {
649 			injectResources(resultPageClass, request, response, resultPage);
650 			injectSessionAttributes(resultPageClass, request, resultPage);
651 			executePostConstruct(resultPageClass, resultPage);
652 		} catch (Exception e) {
653 			LOG.debug("error: " + e.getMessage());
654 			LOG.debug("exception type: " + e.getClass().getName());
655 			writer.println(e.getMessage());
656 			return;
657 		}
658 
659 		LOG.debug("result page class: " + resultPageClass.getSimpleName());
660 
661 		outputPage(resultPageClass, resultPage, writer, session);
662 	}
663 
664 	private Map<String, String> blameInputFields(Object page,
665 			Throwable exception) {
666 		Map<String, String> constraintMessages = new HashMap<String, String>();
667 		Class<?> pageClass = page.getClass();
668 		Field[] fields = pageClass.getDeclaredFields();
669 		LOG.debug("exception type: " + exception.getClass().getName());
670 		for (Field field : fields) {
671 			Input inputAnnotation = field.getAnnotation(Input.class);
672 			if (null == inputAnnotation) {
673 				continue;
674 			}
675 			BlameMe blameMeAnnotation = field.getAnnotation(BlameMe.class);
676 			if (null == blameMeAnnotation) {
677 				continue;
678 			}
679 			LOG.debug("blame input field found: " + field.getName());
680 			for (Class<? extends Exception> exceptionClass : blameMeAnnotation
681 					.value()) {
682 				if (exceptionClass.equals(exception.getClass())) {
683 					String fieldName = field.getName();
684 					LOG.debug(fieldName + ": blame me for exception: "
685 							+ exceptionClass.getName());
686 					constraintMessages.put(fieldName, exception.getMessage());
687 				}
688 			}
689 		}
690 		return constraintMessages;
691 	}
692 
693 	private void saveSessionAttribute(Class<?> pageClass, Object page,
694 			HttpSession session) {
695 		Field[] fields = pageClass.getDeclaredFields();
696 		for (Field field : fields) {
697 			SessionAttribute sessionAttributeAnnotation = field
698 					.getAnnotation(SessionAttribute.class);
699 			if (null == sessionAttributeAnnotation) {
700 				continue;
701 			}
702 			String attributeName = sessionAttributeAnnotation.value();
703 			if ("".equals(attributeName)) {
704 				attributeName = field.getName();
705 			}
706 			field.setAccessible(true);
707 			Object attributeValue = null;
708 			try {
709 				attributeValue = field.get(page);
710 			} catch (Exception e) {
711 				LOG.debug("error reading session attribute field: "
712 						+ field.getName());
713 			}
714 			LOG.debug("saving session attribute:" + attributeName);
715 			session.setAttribute(attributeName, attributeValue);
716 		}
717 	}
718 
719 	public static final String USERNAME_SESSION_ATTRIBUTE = BimboServlet.class
720 			.getName()
721 			+ ".USERNAME";
722 
723 	public <T> T createPage(Class<T> pageClass, HttpServletRequest request,
724 			HttpServletResponse response) throws InstantiationException,
725 			IllegalAccessException, IllegalArgumentException, NamingException,
726 			ServletException {
727 		LOG.debug("create page: " + pageClass.getSimpleName());
728 		T page = pageClass.newInstance();
729 		injectResources(pageClass, request, response, page);
730 		injectSessionAttributes(pageClass, request, page);
731 		executePostConstruct(pageClass, page);
732 		return page;
733 	}
734 
735 	private void injectSessionAttributes(Class<?> pageClass,
736 			HttpServletRequest request, Object page)
737 			throws IllegalArgumentException, IllegalAccessException {
738 		Field[] fields = pageClass.getDeclaredFields();
739 		HttpSession session = null;
740 		for (Field field : fields) {
741 			SessionAttribute sessionAttributeAnnotation = field
742 					.getAnnotation(SessionAttribute.class);
743 			if (null == sessionAttributeAnnotation) {
744 				continue;
745 			}
746 			String attributeName = sessionAttributeAnnotation.value();
747 			if ("".equals(attributeName)) {
748 				attributeName = field.getName();
749 			}
750 			if (null == session) {
751 				session = request.getSession();
752 			}
753 			Object attributeValue = session.getAttribute(attributeName);
754 			field.setAccessible(true);
755 			LOG.debug("restore session attribute: " + attributeName);
756 			field.set(page, attributeValue);
757 		}
758 	}
759 
760 	private void executePostConstruct(Class<?> pageClass, Object page)
761 			throws ServletException {
762 		Method[] methods = pageClass.getDeclaredMethods();
763 		for (Method method : methods) {
764 			PostConstruct postConstructAnnotation = method
765 					.getAnnotation(PostConstruct.class);
766 			if (null == postConstructAnnotation) {
767 				continue;
768 			}
769 			try {
770 				LOG.debug("invoking @PostConstruct: " + method.getName());
771 				method.setAccessible(true);
772 				method.invoke(page, new Object[] {});
773 			} catch (Exception e) {
774 				LOG.debug("error invoking postcontruct method: "
775 						+ e.getMessage(), e);
776 				throw new ServletException(
777 						"Could not execute PostConstruct method: "
778 								+ method.getName());
779 			}
780 		}
781 	}
782 
783 	private void injectResources(Class<?> pageClass,
784 			HttpServletRequest request, HttpServletResponse response,
785 			Object page) throws NamingException, IllegalArgumentException,
786 			IllegalAccessException, ServletException {
787 		Field[] fields = pageClass.getDeclaredFields();
788 		InitialContext initialContext = null;
789 		for (Field field : fields) {
790 			Resource resourceAnnotation = field.getAnnotation(Resource.class);
791 			if (null == resourceAnnotation) {
792 				continue;
793 			}
794 			if (HttpSession.class.equals(field.getType())) {
795 				HttpSession session = request.getSession();
796 				LOG.debug("injecting http session into field "
797 						+ field.getName());
798 				field.setAccessible(true);
799 				field.set(page, session);
800 				continue;
801 			}
802 			if (HttpServletRequest.class.equals(field.getType())) {
803 				LOG.debug("injecting http request into field "
804 						+ field.getName());
805 				field.setAccessible(true);
806 				field.set(page, request);
807 				continue;
808 			}
809 			if (HttpServletResponse.class.equals(field.getType())) {
810 				LOG.debug("injecting http response into field "
811 						+ field.getName());
812 				field.setAccessible(true);
813 				field.set(page, response);
814 				continue;
815 			}
816 			if (Principal.class.equals(field.getType())) {
817 				LOG.debug("injecting principal into field: " + field.getName());
818 				HttpSession session = request.getSession();
819 				String username = (String) session
820 						.getAttribute(USERNAME_SESSION_ATTRIBUTE);
821 				if (null == username) {
822 					username = "anonymous";
823 				}
824 				BimboPrincipal principal = new BimboPrincipal(username);
825 				field.setAccessible(true);
826 				field.set(page, principal);
827 				continue;
828 			}
829 			if (BimboContext.class.equals(field.getType())) {
830 				LOG.debug("injecting this bimbo context");
831 				field.setAccessible(true);
832 				field.set(page, this);
833 				continue;
834 			}
835 			String resourceName = resourceAnnotation.name();
836 			if (null == initialContext) {
837 				initialContext = new InitialContext();
838 			}
839 			Object resource;
840 			try {
841 				resource = initialContext.lookup(resourceName);
842 			} catch (NoInitialContextException e) {
843 				LOG.debug("no initial context: " + e.getMessage());
844 				throw new ServletException("no initial context");
845 			}
846 			if (null == resource) {
847 				throw new ServletException("resource " + resourceName
848 						+ " is null");
849 			}
850 			LOG.debug("injecting resource " + resourceName + " into field "
851 					+ field.getName());
852 			field.setAccessible(true);
853 			field.set(page, resource);
854 		}
855 	}
856 
857 	/**
858 	 * @param pageClass
859 	 * @param page
860 	 * @return map with constraint violation error messages per field.
861 	 * @throws ServletException
862 	 */
863 	private Map<String, String> checkConstraints(Class<?> pageClass, Object page)
864 			throws ServletException {
865 		Map<String, String> constraintViolations = new HashMap<String, String>();
866 		Field[] fields = pageClass.getDeclaredFields();
867 		LOG.debug("check constraints: " + pageClass.getSimpleName());
868 		for (Field field : fields) {
869 			Constraint constraintAnnotation = field
870 					.getAnnotation(Constraint.class);
871 			if (null == constraintAnnotation) {
872 				continue;
873 			}
874 			if (constraintAnnotation.required()) {
875 				LOG.debug("constraint check for field: " + field.getName());
876 				field.setAccessible(true);
877 				Object value;
878 				try {
879 					value = field.get(page);
880 				} catch (Exception e) {
881 					throw new ServletException("could not read field: "
882 							+ field.getName() + " of type "
883 							+ field.getType().getName());
884 				}
885 				if (String.class.equals(field.getType())) {
886 					String strValue = (String) value;
887 					if ("".equals(strValue.trim())) {
888 						LOG.debug("field " + field.getName() + " is required");
889 						constraintViolations.put(field.getName(),
890 								"Value required.");
891 					}
892 				} else if (field.getType().isPrimitive()) {
893 					/*
894 					 * A primitive is always present.
895 					 */
896 				} else {
897 					throw new ServletException(
898 							"constraint violation: field type not supported: "
899 									+ field.getType().getName());
900 				}
901 			}
902 		}
903 		return constraintViolations;
904 	}
905 
906 	/**
907 	 * @param request
908 	 * @param pageClass
909 	 * @param page
910 	 * @return map with conversion error messages per field
911 	 * @throws ServletException
912 	 */
913 	private Map<String, String> restorePageFromRequest(
914 			HttpServletRequest request, Class<?> pageClass, Object page)
915 			throws ServletException {
916 		LOG.debug("restore page: " + pageClass.getSimpleName());
917 		Map<String, String> conversionErrors = new HashMap<String, String>();
918 		Field[] fields = pageClass.getDeclaredFields();
919 		for (Field field : fields) {
920 			if (Modifier.FINAL == (field.getModifiers() & Modifier.FINAL)) {
921 				continue;
922 			}
923 			Input inputAnnotation = field.getAnnotation(Input.class);
924 			Output outputAnnotation = field.getAnnotation(Output.class);
925 			if (null != inputAnnotation || null != outputAnnotation) {
926 				String fieldName = field.getName();
927 				LOG.debug("restore field: " + fieldName);
928 				Render renderAnnotation = field.getAnnotation(Render.class);
929 				if (null != renderAnnotation) {
930 					Class<? extends Renderer<?>> rendererClass = renderAnnotation
931 							.value();
932 					Renderer<?> renderer = null;
933 					try {
934 						renderer = rendererClass.newInstance();
935 					} catch (Exception e) {
936 						throw new ServletException(
937 								"could not init renderer class: "
938 										+ rendererClass.getName());
939 					}
940 					if (null != renderer) {
941 						Object value;
942 						try {
943 							value = renderer.restore(fieldName, request);
944 						} catch (ConversionException e) {
945 							conversionErrors.put(fieldName, e.getMessage());
946 							continue;
947 						}
948 						field.setAccessible(true);
949 						try {
950 							field.set(page, value);
951 						} catch (Exception e) {
952 							throw new ServletException("could not set field: "
953 									+ fieldName, e);
954 						}
955 					}
956 					continue;
957 				} else if (List.class.equals(field.getType())) {
958 					LOG.debug("table detected");
959 					String tableType = request
960 							.getParameter(fieldName + ".type");
961 					List<Object> list = new LinkedList<Object>();
962 					if (null != tableType) {
963 						Class<?> tableEntryClass = loadClass(tableType);
964 						if (String.class.equals(tableEntryClass)) {
965 							int idx = 0;
966 							String value;
967 							while (null != (value = request
968 									.getParameter(fieldName + "." + idx))) {
969 								list.add(value);
970 								idx++;
971 							}
972 						} else {
973 							int idx = 0;
974 							while (null != (request.getParameter(fieldName
975 									+ "." + idx))) {
976 								Object tableEntry;
977 								try {
978 									tableEntry = tableEntryClass.newInstance();
979 								} catch (Exception e) {
980 									LOG.debug("could not init table entry: "
981 											+ tableEntryClass.getName());
982 									throw new ServletException(
983 											"could not init table entry: "
984 													+ tableEntryClass.getName());
985 								}
986 								Field[] tableEntryFields = tableEntryClass
987 										.getDeclaredFields();
988 								for (Field tableEntryField : tableEntryFields) {
989 									if (null != tableEntryField
990 											.getAnnotation(Output.class)) {
991 										String pageValue = request
992 												.getParameter(fieldName
993 														+ "."
994 														+ idx
995 														+ "."
996 														+ tableEntryField
997 																.getName());
998 										Object value;
999 										if (Integer.TYPE.equals(tableEntryField
1000 												.getType())) {
1001 											value = Integer.parseInt(pageValue);
1002 										} else if (Float.TYPE
1003 												.equals(tableEntryField
1004 														.getType())) {
1005 											value = Float.parseFloat(pageValue);
1006 										} else if (Double.TYPE
1007 												.equals(tableEntryField
1008 														.getType())) {
1009 											value = Double
1010 													.parseDouble(pageValue);
1011 										} else {
1012 											value = pageValue;
1013 										}
1014 										tableEntryField.setAccessible(true);
1015 										try {
1016 											tableEntryField.set(tableEntry,
1017 													value);
1018 										} catch (Exception e) {
1019 											LOG
1020 													.debug("table entry field: could not set field: "
1021 															+ tableEntryField
1022 																	.getName());
1023 											throw new ServletException(
1024 													"table entry field: could not set field: "
1025 															+ tableEntryField
1026 																	.getName());
1027 										}
1028 									}
1029 								}
1030 								list.add(tableEntry);
1031 								idx++;
1032 							}
1033 						}
1034 					}
1035 					field.setAccessible(true);
1036 					try {
1037 						LOG.debug("restore table field: " + fieldName);
1038 						field.set(page, list);
1039 					} catch (Exception e) {
1040 						LOG.debug("restore table field: could not set field: "
1041 								+ fieldName);
1042 						throw new ServletException(
1043 								"restore table field: could not set field: "
1044 										+ fieldName);
1045 					}
1046 					continue;
1047 				}
1048 				String fieldPageValue = request.getParameter(fieldName);
1049 				Object fieldValue;
1050 				Class<? extends Renderer<?>> rendererClass = typeRenderers
1051 						.get(field.getType());
1052 				if (null != rendererClass) {
1053 					Renderer<?> renderer;
1054 					try {
1055 						renderer = rendererClass.newInstance();
1056 					} catch (Exception e) {
1057 						throw new ServletException(
1058 								"could not init renderer class: "
1059 										+ rendererClass.getName());
1060 					}
1061 					try {
1062 						fieldValue = renderer.restore(fieldName, request);
1063 					} catch (ConversionException e) {
1064 						conversionErrors.put(fieldName, e.getMessage());
1065 						continue;
1066 					}
1067 				} else if (field.getType().isEnum()) {
1068 					Object[] enumConstants = field.getType().getEnumConstants();
1069 					fieldValue = null;
1070 					for (Object enumConstant : enumConstants) {
1071 						Enum<?> enumClass = (Enum<?>) enumConstant;
1072 						if (fieldPageValue.equals(enumClass.name())) {
1073 							fieldValue = enumConstant;
1074 							break;
1075 						}
1076 					}
1077 					if (null == fieldValue) {
1078 						throw new ServletException("invalid enum value: "
1079 								+ fieldPageValue);
1080 					}
1081 				} else if (hasOutputFields(field.getType())) {
1082 					fieldValue = restoreFromRecord(request, field);
1083 				} else {
1084 					LOG.debug("restore page: field type not supported: "
1085 							+ field.getType());
1086 					throw new ServletException(
1087 							"restore page: field type not supported: "
1088 									+ field.getType());
1089 				}
1090 				field.setAccessible(true);
1091 				try {
1092 					field.set(page, fieldValue);
1093 				} catch (Exception e) {
1094 					LOG.debug("restore: could not set field: " + fieldName);
1095 					throw new ServletException("restore: could not set field: "
1096 							+ fieldName);
1097 				}
1098 			}
1099 		}
1100 		LOG.debug("page restored");
1101 		return conversionErrors;
1102 	}
1103 
1104 	private Object restoreFromRecord(HttpServletRequest request, Field field)
1105 			throws ServletException {
1106 		Object fieldValue;
1107 		try {
1108 			fieldValue = field.getType().newInstance();
1109 		} catch (Exception e) {
1110 			throw new ServletException("cannot init type: "
1111 					+ field.getType().getName());
1112 		}
1113 		Field[] recordFields = field.getType().getDeclaredFields();
1114 		for (Field recordField : recordFields) {
1115 			Output recordOutputAnnotation = recordField
1116 					.getAnnotation(Output.class);
1117 			if (null == recordOutputAnnotation) {
1118 				continue;
1119 			}
1120 			LOG.debug("restore " + field.getName() + "."
1121 					+ recordField.getName());
1122 			Class<? extends Renderer<?>> rendererClass = typeRenderers
1123 					.get(recordField.getType());
1124 			if (null == rendererClass) {
1125 				throw new ServletException("no renderer class for type: "
1126 						+ recordField.getType());
1127 			}
1128 			Renderer<?> renderer;
1129 			try {
1130 				renderer = rendererClass.newInstance();
1131 			} catch (Exception e) {
1132 				throw new ServletException("renderer init error: "
1133 						+ e.getMessage(), e);
1134 			}
1135 			Object recordValue;
1136 			try {
1137 				recordValue = renderer.restore(field.getName() + "."
1138 						+ recordField.getName(), request);
1139 			} catch (ConversionException e) {
1140 				LOG.debug("error restoring from record: " + e.getMessage(), e);
1141 				throw new ServletException("error restoring from record: "
1142 						+ field.getName(), e);
1143 			}
1144 			recordField.setAccessible(true);
1145 			try {
1146 				recordField.set(fieldValue, recordValue);
1147 			} catch (Exception e) {
1148 				throw new ServletException("error on restore record field: "
1149 						+ recordField.getName());
1150 			}
1151 		}
1152 		return fieldValue;
1153 	}
1154 
1155 	private void writePageContent(Class<?> pageClass, Object pageObject,
1156 			Map<String, String> constraintViolations, String errorMessage,
1157 			PrintWriter writer) {
1158 		LOG.debug("write page content for page: " + pageClass.getSimpleName());
1159 		outputFields(pageClass, pageObject, constraintViolations, writer);
1160 		outputMessages(writer, errorMessage);
1161 		outputActions(pageClass, writer);
1162 	}
1163 
1164 	private void outputFields(Class<?> pageClass, Object pageObject,
1165 			Map<String, String> constraintViolations, PrintWriter writer) {
1166 		writer.println("<table>");
1167 		Field[] fields = pageClass.getDeclaredFields();
1168 		for (Field field : fields) {
1169 			Output outputAnnotation = field.getAnnotation(Output.class);
1170 			if (null != outputAnnotation) {
1171 				writeOutputField(field, outputAnnotation, pageClass,
1172 						pageObject, writer);
1173 			}
1174 			Input inputAnnotation = field.getAnnotation(Input.class);
1175 			if (null != inputAnnotation) {
1176 				String fieldName = field.getName();
1177 				String constraintViolation;
1178 				if (constraintViolations.containsKey(fieldName)) {
1179 					constraintViolation = constraintViolations.get(fieldName);
1180 					if (null == constraintViolation) {
1181 						/*
1182 						 * Even in case of a null error messsage we have to be
1183 						 * able to report something.
1184 						 */
1185 						constraintViolation = "null";
1186 					}
1187 				} else {
1188 					constraintViolation = null;
1189 				}
1190 				writeInputField(field, inputAnnotation, pageObject,
1191 						constraintViolation, writer);
1192 			}
1193 			// XXX: what if a field is both @Input and @Output
1194 		}
1195 		writer.println("</table>");
1196 	}
1197 
1198 	private void outputActions(Class<?> pageClass, PrintWriter writer) {
1199 		writer.println("<div>");
1200 		Method[] methods = pageClass.getDeclaredMethods();
1201 		for (Method method : methods) {
1202 			Action actionAnnotation = method.getAnnotation(Action.class);
1203 			if (null == actionAnnotation) {
1204 				continue;
1205 			}
1206 			if (0 != method.getParameterTypes().length) {
1207 				/*
1208 				 * In this case the action is a table action.
1209 				 */
1210 				continue;
1211 			}
1212 			String methodName = method.getName();
1213 			String actionName = actionAnnotation.value();
1214 			if ("".equals(actionName)) {
1215 				actionName = methodName;
1216 			}
1217 			HtmlElement inputElement = new HtmlElement("input").addAttribute(
1218 					"type", "button").addAttribute("name", methodName)
1219 					.addAttribute("value", actionName);
1220 			String confirmationMessage = actionAnnotation.confirmation();
1221 			Class<?> returnType = method.getReturnType();
1222 			if (Void.TYPE.equals(returnType)) {
1223 				LOG.debug("ajax method");
1224 				inputElement.addAttribute("onclick", "doAjaxAction('"
1225 						+ methodName + "');");
1226 			} else {
1227 				if ("".equals(confirmationMessage)) {
1228 					inputElement.addAttribute("onclick", "doAction('"
1229 							+ methodName + "');");
1230 				} else {
1231 					inputElement
1232 							.addAttribute("onclick", "doConfirmAction('"
1233 									+ methodName + "', '" + confirmationMessage
1234 									+ "');");
1235 				}
1236 			}
1237 			inputElement.write(writer);
1238 		}
1239 		writer.println("</div>");
1240 	}
1241 
1242 	private void outputMessages(PrintWriter writer, String message) {
1243 		/*
1244 		 * Important that the div has an empty body.
1245 		 */
1246 		writer.println("<div id=\"bimbo.ajax.message\"></div>");
1247 		if (null == message) {
1248 			return;
1249 		}
1250 		writer.println("<p>");
1251 		{
1252 			new HtmlElement("div")
1253 					.addAttribute(
1254 							"style",
1255 							"color: red; background-color: #ffe0e0; display: inline; border-style: solid; border-width: 1px; border-color: red; padding-left: 5px; padding-right: 5px;")
1256 					.setBody(message).write(writer);
1257 		}
1258 		writer.println("</p>");
1259 	}
1260 
1261 	private void writeOutputField(Field field, Output outputAnnotation,
1262 			Class<?> pageClass, Object pageObject, PrintWriter writer) {
1263 		writer.println("<tr>");
1264 		{
1265 			String outputLabel = outputAnnotation.value();
1266 			if (false == "".equals(outputLabel)) {
1267 				new HtmlElement("th").addAttribute("align", "left")
1268 						.addAttribute("style", "background-color: #e0e0e0")
1269 						.setBody(outputLabel + ":").write(writer);
1270 				writer.println("<td>");
1271 			} else {
1272 				writer.println("<td colspan=\"2\">");
1273 			}
1274 			String fieldName = field.getName();
1275 			try {
1276 				field.setAccessible(true);
1277 				Object outputValue = field.get(pageObject);
1278 				Render renderAnnotation = field.getAnnotation(Render.class);
1279 				if (null != renderAnnotation) {
1280 					Class<? extends Renderer<?>> rendererClass = renderAnnotation
1281 							.value();
1282 					Renderer renderer = rendererClass.newInstance();
1283 					renderer.renderOutput(fieldName, outputValue, writer);
1284 				} else if (outputAnnotation.verbatim()) {
1285 					new HtmlElement("pre").setBody(outputValue.toString())
1286 							.write(writer);
1287 					new HtmlElement("input").addAttribute("type", "hidden")
1288 							.addAttribute("name", fieldName).addAttribute(
1289 									"value", outputValue.toString()).write(
1290 									writer);
1291 				} else if (List.class.equals(field.getType())) {
1292 					List<?> outputList = (List<?>) outputValue;
1293 					outputTable(fieldName, outputList, pageClass, writer);
1294 				} else {
1295 					Class<?> outputClass;
1296 					if (null != outputValue) {
1297 						outputClass = outputValue.getClass();
1298 					} else {
1299 						outputClass = field.getType();
1300 					}
1301 					if (hasOutputFields(outputClass)) {
1302 						writeOutputRecordField(field, outputValue, outputClass,
1303 								writer);
1304 					} else {
1305 						if (null == outputValue) {
1306 							outputValue = "";
1307 						}
1308 						writer.println(escape(outputValue));
1309 						new HtmlElement("input").addAttribute("type", "hidden")
1310 								.addAttribute("name", fieldName).addAttribute(
1311 										"value", outputValue.toString()).write(
1312 										writer);
1313 					}
1314 				}
1315 			} catch (Exception e) {
1316 				LOG.debug("cannot read field " + fieldName + ": "
1317 						+ e.getMessage());
1318 				writer.println("cannot read field: " + fieldName);
1319 			}
1320 			writer.println("</td>");
1321 		}
1322 		writer.println("</tr>");
1323 	}
1324 
1325 	private void writeOutputRecordField(Field field, Object outputValue,
1326 			Class<?> outputClass, PrintWriter writer)
1327 			throws IllegalAccessException, InstantiationException {
1328 		writer.println("<table>");
1329 		Field[] recordFields = outputClass.getDeclaredFields();
1330 		for (Field recordField : recordFields) {
1331 			Output recordOutputAnnotation = recordField
1332 					.getAnnotation(Output.class);
1333 			if (null == recordOutputAnnotation) {
1334 				continue;
1335 			}
1336 			writer.println("<tr>");
1337 			{
1338 				String recordLabel = recordOutputAnnotation.value();
1339 				if ("".equals(recordLabel)) {
1340 					recordLabel = recordField.getName();
1341 				}
1342 				new HtmlElement("th").addAttribute("align", "left")
1343 						.addAttribute("style", "background-color: #e0e0e0;")
1344 						.setBody(recordLabel + ":").write(writer);
1345 				recordField.setAccessible(true);
1346 				Object value = recordField.get(outputValue);
1347 				Class<? extends Renderer<?>> rendererClass = typeRenderers
1348 						.get(recordField.getType());
1349 				Renderer renderer = rendererClass.newInstance();
1350 				writer.println("<td>");
1351 				renderer.renderOutput(field.getName() + "."
1352 						+ recordField.getName(), value, writer);
1353 				writer.println("</td>");
1354 			}
1355 			writer.println("</tr>");
1356 		}
1357 		writer.println("</table>");
1358 	}
1359 
1360 	private static final Map<Class<?>, Class<? extends Renderer<?>>> typeRenderers = new HashMap<Class<?>, Class<? extends Renderer<?>>>();
1361 
1362 	static {
1363 		typeRenderers.put(Date.class, DateRenderer.class);
1364 		typeRenderers.put(Boolean.TYPE, BooleanRenderer.class);
1365 		typeRenderers.put(String.class, StringRenderer.class);
1366 		typeRenderers.put(Double.TYPE, DoubleRenderer.class);
1367 		typeRenderers.put(Integer.TYPE, IntegerRenderer.class);
1368 		typeRenderers.put(Float.TYPE, FloatRenderer.class);
1369 	}
1370 
1371 	private void writeConstraintViolation(String constraintViolation,
1372 			String fieldName, PrintWriter writer) {
1373 		if (null == constraintViolation) {
1374 			return;
1375 		}
1376 		new HtmlElement("div")
1377 				.addAttribute("id", "bimbo.error." + fieldName)
1378 				.addAttribute(
1379 						"style",
1380 						"color: red; background-color: #ffe0e0; display: inline; border-style: solid; border-width: 1px; border-color: red; padding-left: 5px; padding-right: 5px;")
1381 				.setBody(constraintViolation).write(writer);
1382 	}
1383 
1384 	private void writeInputField(Field field, Input inputAnnotation,
1385 			Object pageObject, String constraintViolation, PrintWriter writer) {
1386 		String fieldName = field.getName();
1387 		writer.println("<tr>");
1388 		{
1389 			writer
1390 					.println("<th style=\"background-color: #e0e0e0;\" align=\"left\">");
1391 			{
1392 				String inputLabel = inputAnnotation.value();
1393 				if ("".equals(inputLabel)) {
1394 					inputLabel = fieldName;
1395 				}
1396 				writer.println(escape(inputLabel) + ":");
1397 				Constraint constraintAnnotation = field
1398 						.getAnnotation(Constraint.class);
1399 				if (null != constraintAnnotation) {
1400 					if (constraintAnnotation.required()) {
1401 						writer.println("*");
1402 					}
1403 				}
1404 			}
1405 			writer.println("</th>");
1406 
1407 			writer.println("<td>");
1408 			writeInputFieldValue(field, inputAnnotation, pageObject, fieldName,
1409 					writer);
1410 			writeConstraintViolation(constraintViolation, fieldName, writer);
1411 			writer.println("</td>");
1412 		}
1413 		writer.println("</tr>");
1414 	}
1415 
1416 	private void writeInputFieldValue(Field field, Input inputAnnotation,
1417 			Object pageObject, String fieldName, PrintWriter writer) {
1418 		Object initialValue;
1419 		field.setAccessible(true);
1420 		try {
1421 			initialValue = field.get(pageObject);
1422 		} catch (Exception e) {
1423 			LOG.debug("error reading field: " + field.getName());
1424 			return;
1425 		}
1426 		Render renderAnnotation = field.getAnnotation(Render.class);
1427 		if (null != renderAnnotation) {
1428 			Class<? extends Renderer<?>> rendererClass = renderAnnotation
1429 					.value();
1430 			Renderer renderer;
1431 			try {
1432 				renderer = rendererClass.newInstance();
1433 			} catch (Exception e) {
1434 				writer.println("could not init renderer class: "
1435 						+ rendererClass.getName());
1436 				return;
1437 			}
1438 			renderer.renderInput(fieldName, initialValue, inputAnnotation,
1439 					writer);
1440 			return;
1441 		}
1442 		if (typeRenderers.containsKey(field.getType())) {
1443 			Class<? extends Renderer<?>> rendererClass = typeRenderers
1444 					.get(field.getType());
1445 			Renderer renderer;
1446 			try {
1447 				renderer = rendererClass.newInstance();
1448 			} catch (Exception e) {
1449 				writer.println("could not init render class: "
1450 						+ rendererClass.getName());
1451 				return;
1452 			}
1453 			renderer.renderInput(fieldName, initialValue, inputAnnotation,
1454 					writer);
1455 			return;
1456 		}
1457 		if (field.getType().isEnum()) {
1458 			writer.println("<select name=\"" + fieldName + "\">");
1459 			Object[] enumConstants = field.getType().getEnumConstants();
1460 			Enum<?> initialEnum = (Enum<?>) initialValue;
1461 			for (Object enumConstant : enumConstants) {
1462 				Enum<?> enumClass = (Enum<?>) enumConstant;
1463 				HtmlElement optionElement = new HtmlElement("option")
1464 						.addAttribute("value", enumClass.name()).setBody(
1465 								enumConstant.toString());
1466 				if (enumClass == initialEnum) {
1467 					optionElement.addAttribute("selected", "true");
1468 				}
1469 				optionElement.write(writer);
1470 			}
1471 			writer.println("</select>");
1472 			return;
1473 		}
1474 		if (hasOutputFields(field.getType())) {
1475 			try {
1476 				writeInputRecordField(field, initialValue, writer);
1477 			} catch (Exception e) {
1478 				LOG.debug("error writing input record field: "
1479 						+ field.getName(), e);
1480 				writer
1481 						.println("error writing input record: "
1482 								+ field.getName());
1483 			}
1484 			return;
1485 		}
1486 		writer.println("Could not render input field: " + fieldName);
1487 	}
1488 
1489 	private void writeInputRecordField(Field field, Object outputValue,
1490 			PrintWriter writer) throws IllegalArgumentException,
1491 			IllegalAccessException, InstantiationException {
1492 		writer.println("<table>");
1493 		Class<?> outputClass = field.getType();
1494 		Field[] recordFields = outputClass.getDeclaredFields();
1495 		for (Field recordField : recordFields) {
1496 			Output recordOutputAnnotation = recordField
1497 					.getAnnotation(Output.class);
1498 			if (null == recordOutputAnnotation) {
1499 				continue;
1500 			}
1501 			writer.println("<tr>");
1502 			{
1503 				String recordLabel = recordOutputAnnotation.value();
1504 				if ("".equals(recordLabel)) {
1505 					recordLabel = recordField.getName();
1506 				}
1507 				new HtmlElement("th").addAttribute("align", "left")
1508 						.addAttribute("style", "background-color: #e0e0e0;")
1509 						.setBody(recordLabel + ":").write(writer);
1510 				recordField.setAccessible(true);
1511 				Object value;
1512 				if (null != outputValue) {
1513 					value = recordField.get(outputValue);
1514 				} else {
1515 					value = null;
1516 				}
1517 				Class<? extends Renderer<?>> rendererClass = typeRenderers
1518 						.get(recordField.getType());
1519 				Renderer renderer = rendererClass.newInstance();
1520 				writer.println("<td>");
1521 				renderer.renderInput(field.getName() + "."
1522 						+ recordField.getName(), value, null, writer);
1523 				writer.println("</td>");
1524 			}
1525 			writer.println("</tr>");
1526 		}
1527 		writer.println("</table>");
1528 	}
1529 
1530 	private boolean hasOutputFields(Class<?> outputClass) {
1531 		Field[] fields = outputClass.getDeclaredFields();
1532 		for (Field field : fields) {
1533 			if (null != field.getAnnotation(Output.class)) {
1534 				return true;
1535 			}
1536 		}
1537 		return false;
1538 	}
1539 
1540 	private boolean hasFields(Class<?> type) {
1541 		if (String.class.equals(type)) {
1542 			return false;
1543 		}
1544 		Field[] fields = type.getDeclaredFields();
1545 		return fields.length != 0;
1546 	}
1547 
1548 	private void outputTable(String tableName, List<?> list,
1549 			Class<?> pageClass, PrintWriter writer) {
1550 		if (null == list) {
1551 			return;
1552 		}
1553 		if (list.isEmpty()) {
1554 			return;
1555 		}
1556 		writer.println("<table style=\"border: 1 px;\">");
1557 		Class<?> listEntryClass = list.get(0).getClass();
1558 		writer.println("<input type=\"hidden\" name=\"" + tableName
1559 				+ ".type\" value=\"" + listEntryClass.getName() + "\"/>");
1560 		if (hasFields(listEntryClass)) {
1561 			writer.println("<tr>");
1562 			{
1563 				Field[] fields = listEntryClass.getDeclaredFields();
1564 				for (Field field : fields) {
1565 					Output outputAnnotation = field.getAnnotation(Output.class);
1566 					String label;
1567 					if (null != outputAnnotation) {
1568 						label = outputAnnotation.value();
1569 					} else {
1570 						label = field.getName();
1571 					}
1572 					new HtmlElement("th").addAttribute("style",
1573 							"background-color: #a0a0a0;").setBody(label).write(
1574 							writer);
1575 				}
1576 			}
1577 			writer.println("</tr>");
1578 		}
1579 		int size = list.size();
1580 		for (int idx = 0; idx < size; idx++) {
1581 			Object item = list.get(idx);
1582 			writer.println("<tr>");
1583 			{
1584 				{
1585 					if (hasFields(listEntryClass)) {
1586 						writer.println("<input type=\"hidden\" name=\""
1587 								+ tableName + "." + idx + "\" value=\""
1588 								+ listEntryClass.getName() + "\" />");
1589 						Field[] fields = listEntryClass.getDeclaredFields();
1590 						for (Field field : fields) {
1591 							Object fieldValue;
1592 							try {
1593 								field.setAccessible(true);
1594 								fieldValue = field.get(item);
1595 							} catch (Exception e) {
1596 								writer.println("Could not read field: "
1597 										+ field.getName());
1598 								continue;
1599 							}
1600 							HtmlElement tdElement = new HtmlElement("td")
1601 									.setBody(fieldValue.toString());
1602 							if (0 != idx % 2) {
1603 								tdElement.addAttribute("style",
1604 										"background-color: #e0e0e0;");
1605 							}
1606 							tdElement.write(writer);
1607 							new HtmlElement("input").addAttribute("type",
1608 									"hidden").addAttribute(
1609 									"name",
1610 									tableName + "." + idx + "."
1611 											+ field.getName()).addAttribute(
1612 									"value", fieldValue.toString()).write(
1613 									writer);
1614 						}
1615 					} else {
1616 						new HtmlElement("td").setBody(item.toString()).write(
1617 								writer);
1618 						new HtmlElement("input").addAttribute("type", "hidden")
1619 								.addAttribute("name", tableName + "." + idx)
1620 								.addAttribute("value", item.toString()).write(
1621 										writer);
1622 					}
1623 				}
1624 			}
1625 			Method[] methods = pageClass.getDeclaredMethods();
1626 			for (Method method : methods) {
1627 				Action actionAnnotation = method.getAnnotation(Action.class);
1628 				if (null == actionAnnotation) {
1629 					continue;
1630 				}
1631 				if (1 != method.getParameterTypes().length) {
1632 					continue;
1633 				}
1634 				/*
1635 				 * The method is table action.
1636 				 */
1637 				Class<?> methodParamType = method.getParameterTypes()[0];
1638 				if (false == methodParamType.equals(listEntryClass)) {
1639 					continue;
1640 				}
1641 				String actionName = actionAnnotation.value();
1642 				if ("".equals(actionName)) {
1643 					actionName = method.getName();
1644 				}
1645 				writer.println("<td>");
1646 				{
1647 					new HtmlElement("input").addAttribute("type", "button")
1648 							.addAttribute("value", actionName).addAttribute(
1649 									"name",
1650 									tableName + "." + method.getName() + "."
1651 											+ idx).addAttribute(
1652 									"onclick",
1653 									"doAction('" + method.getName() + "("
1654 											+ tableName + "." + idx + ")"
1655 											+ "')").write(writer);
1656 				}
1657 				writer.println("</td>");
1658 			}
1659 			writer.println("</tr>");
1660 		}
1661 		writer.println("</table>");
1662 	}
1663 
1664 	private String getTitle(Class<?> pageClass) {
1665 		Title titleAnnotation = pageClass.getAnnotation(Title.class);
1666 		if (null == titleAnnotation) {
1667 			return pageClass.getSimpleName();
1668 		}
1669 		return titleAnnotation.value();
1670 	}
1671 }