001/* 002 * Shredzone Commons 003 * 004 * Copyright (C) 2012 Richard "Shred" Körber 005 * http://commons.shredzone.org 006 * 007 * This program is free software: you can redistribute it and/or modify 008 * it under the terms of the GNU Library General Public License as 009 * published by the Free Software Foundation, either version 3 of the 010 * License, or (at your option) any later version. 011 * 012 * This program is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 015 * GNU General Public License for more details. 016 * 017 * You should have received a copy of the GNU Library General Public License 018 * along with this program. If not, see <http://www.gnu.org/licenses/>. 019 */ 020 021package org.shredzone.commons.view.manager; 022 023import java.lang.annotation.Annotation; 024import java.lang.reflect.Method; 025import java.lang.reflect.UndeclaredThrowableException; 026 027import javax.annotation.Nonnull; 028import javax.annotation.ParametersAreNonnullByDefault; 029import javax.annotation.concurrent.Immutable; 030import javax.servlet.ServletRequest; 031import javax.servlet.http.HttpServletRequest; 032import javax.servlet.http.HttpSession; 033import javax.swing.text.View; 034 035import org.shredzone.commons.view.ViewContext; 036import org.shredzone.commons.view.annotation.Attribute; 037import org.shredzone.commons.view.annotation.Cookie; 038import org.shredzone.commons.view.annotation.Optional; 039import org.shredzone.commons.view.annotation.Parameter; 040import org.shredzone.commons.view.annotation.PathPart; 041import org.shredzone.commons.view.annotation.Qualifier; 042import org.shredzone.commons.view.annotation.SessionId; 043import org.shredzone.commons.view.annotation.ViewHandler; 044import org.shredzone.commons.view.exception.PageNotFoundException; 045import org.shredzone.commons.view.exception.ViewContextException; 046import org.shredzone.commons.view.exception.ViewException; 047import org.slf4j.Logger; 048import org.slf4j.LoggerFactory; 049import org.springframework.core.convert.ConversionService; 050import org.springframework.core.convert.TypeDescriptor; 051import org.springframework.util.ReflectionUtils; 052 053/** 054 * Keeps a reference to and invokes a view handler. 055 * <p> 056 * {@link ViewInvoker ViewInvokers} are immutable. 057 * 058 * @author Richard "Shred" Körber 059 */ 060@ParametersAreNonnullByDefault 061@Immutable 062public class ViewInvoker { 063 private static final Logger LOG = LoggerFactory.getLogger(ViewInvoker.class); 064 065 private final Object bean; 066 private final Method method; 067 private final ConversionService conversionService; 068 private final Annotation[] viewAnnotations; 069 private final boolean[] optionals; 070 071 /** 072 * Creates a new {@link ViewInvoker}. 073 * 074 * @param bean 075 * target Spring bean to be invoked 076 * @param method 077 * target method to be invoked 078 * @param conversionService 079 * {@link ConversionService} to be used for parameter conversion 080 */ 081 public ViewInvoker(Object bean, Method method, ConversionService conversionService) { 082 this.bean = bean; 083 this.method = method; 084 this.conversionService = conversionService; 085 086 Annotation[][] annotations = method.getParameterAnnotations(); 087 viewAnnotations = new Annotation[annotations.length]; 088 optionals = new boolean[annotations.length]; 089 090 for (int ix = 0; ix < annotations.length; ix++) { 091 for (Annotation sub : annotations[ix]) { 092 if ( sub instanceof PathPart 093 || sub instanceof Parameter 094 || sub instanceof Attribute 095 || sub instanceof Cookie 096 || sub instanceof SessionId 097 || sub instanceof Qualifier) { 098 if (viewAnnotations[ix] != null) { 099 throw new IllegalArgumentException("Conflicting annotations " 100 + sub + " and " + viewAnnotations[ix] + " in view handler " 101 + bean.getClass().getName() + "#" + method.getName() + "()"); 102 } 103 viewAnnotations[ix] = sub; 104 } 105 106 if ( sub instanceof Optional 107 || sub instanceof SessionId 108 || sub instanceof Qualifier) { 109 optionals[ix] = true; 110 } 111 } 112 } 113 } 114 115 /** 116 * The Spring bean that was annotated with {@link ViewHandler}. 117 */ 118 public @Nonnull Object getBean() { return bean; } 119 120 /** 121 * The target view handler method that was annotated with {@link View}. 122 */ 123 public @Nonnull Method getMethod() { return method; } 124 125 /** 126 * Invokes the view handler. 127 * 128 * @param context 129 * {@link ViewContext} containing all necessary data for invoking the view 130 * @return String returned by the view handler. Usually this is a reference to a JSP 131 * that is used for rendering the result. If {@code null}, the view handler 132 * took care for sending a response itself. 133 */ 134 public String invoke(ViewContext context) throws ViewException { 135 Class<?>[] types = method.getParameterTypes(); 136 Object[] values = new Object[types.length]; 137 138 for (int ix = 0; ix < types.length; ix++) { 139 Object result = evaluateParameter(types[ix], viewAnnotations[ix], optionals[ix], context); 140 if (result == null && !optionals[ix]) { 141 throw new PageNotFoundException("Argument " + ix + " is required but missing."); 142 } 143 values[ix] = result; 144 } 145 146 try { 147 Object renderViewName = ReflectionUtils.invokeMethod(method, bean, values); 148 return renderViewName != null ? renderViewName.toString() : null; 149 } catch (UndeclaredThrowableException|IllegalStateException ex) { 150 Throwable cause = ex.getCause(); 151 if (cause instanceof ViewException) { 152 throw (ViewException) cause; 153 } else { 154 throw ex; 155 } 156 } 157 } 158 159 /** 160 * Evaluates a single parameter of the handler method's parameter list. 161 * 162 * @param type 163 * Expected parameter type 164 * @param anno 165 * {@link Annotation} of this parameter 166 * @param optional 167 * if this parameter is optional and may be {@code null} 168 * @param context 169 * {@link ViewContext} containing all necessary data for invoking the view 170 * @return Parameter value to be passed to the method 171 */ 172 private Object evaluateParameter(Class<?> type, Annotation anno, boolean optional, ViewContext context) 173 throws ViewException { 174 175 if (anno instanceof Parameter) { 176 String name = ((Parameter) anno).value(); 177 String value = context.getParameter(name); 178 if (value == null && !optional) { 179 throw new ViewContextException("Missing parameter " + name); 180 } 181 return conversionService.convert(value, type); 182 } 183 184 if (anno instanceof PathPart) { 185 String part = ((PathPart) anno).value(); 186 String value = context.getPathParts().get(part); 187 if (value != null) { 188 return conversionService.convert(value, type); 189 } else if (optional) { 190 return conversionService.convert(null, 191 TypeDescriptor.valueOf(String.class), 192 TypeDescriptor.valueOf(type)); 193 } else { 194 throw new ViewException("Unsatisfied path part: " + part); 195 } 196 } 197 198 if (anno instanceof Attribute) { 199 String name = ((Attribute) anno).value(); 200 ServletRequest req = context.getValueOfType(ServletRequest.class); 201 Object value = req.getAttribute(name); 202 if (value == null && !optional) { 203 throw new ViewContextException("Missing attribute " + name); 204 } 205 return conversionService.convert(value, type); 206 } 207 208 if (anno instanceof Cookie) { 209 String name = ((Cookie) anno).value(); 210 HttpServletRequest req = context.getValueOfType(HttpServletRequest.class); 211 for (javax.servlet.http.Cookie cookie : req.getCookies()) { 212 if (name.equals(cookie.getName())) { 213 return conversionService.convert(cookie.getValue(), type); 214 } 215 } 216 if (optional) { 217 return conversionService.convert(null, 218 TypeDescriptor.valueOf(String.class), 219 TypeDescriptor.valueOf(type)); 220 } else { 221 throw new ViewException("Cookie not set: " + name); 222 } 223 } 224 225 if (anno instanceof SessionId) { 226 HttpSession session = context.getValueOfType(HttpSession.class); 227 if (session != null) { 228 return conversionService.convert(session.getId(), type); 229 } else { 230 return conversionService.convert(null, 231 TypeDescriptor.valueOf(String.class), 232 TypeDescriptor.valueOf(type)); 233 } 234 } 235 236 if (anno instanceof Qualifier) { 237 // Qualifiers are always optional 238 return conversionService.convert(context.getQualifier(), type); 239 } 240 241 // Finally, try to get an object of that type from the data provider 242 try { 243 return context.getValueOfType(type); 244 } catch (ViewContextException ex) { 245 // ignore and continue... 246 LOG.debug("Failed to get value of type {} from context", type, ex); 247 } 248 249 // Who the heck would need this... 250 if (ViewContext.class.isAssignableFrom(type)) { 251 return context; 252 } 253 254 // Alas, we cannot find anything to satisfy this parameter 255 throw new ViewContextException("Unknown parameter type " + type.getName()); 256 } 257 258}