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}