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.impl;
022
023import java.io.IOException;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.Map;
027
028import javax.annotation.Nonnull;
029import javax.annotation.ParametersAreNonnullByDefault;
030import javax.annotation.PostConstruct;
031import javax.annotation.Resource;
032import javax.servlet.RequestDispatcher;
033import javax.servlet.ServletContext;
034import javax.servlet.ServletException;
035import javax.servlet.http.HttpServletRequest;
036import javax.servlet.http.HttpServletResponse;
037
038import org.shredzone.commons.view.PathContext;
039import org.shredzone.commons.view.PathType;
040import org.shredzone.commons.view.ViewContext;
041import org.shredzone.commons.view.ViewInterceptor;
042import org.shredzone.commons.view.ViewService;
043import org.shredzone.commons.view.exception.ErrorResponseException;
044import org.shredzone.commons.view.exception.PageNotFoundException;
045import org.shredzone.commons.view.exception.ViewException;
046import org.shredzone.commons.view.manager.ViewInvoker;
047import org.shredzone.commons.view.manager.ViewManager;
048import org.shredzone.commons.view.manager.ViewPattern;
049import org.shredzone.commons.view.util.ViewPathEvaluationContext;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052import org.springframework.context.ApplicationContext;
053import org.springframework.core.convert.ConversionService;
054import org.springframework.expression.EvaluationContext;
055import org.springframework.expression.spel.support.StandardTypeConverter;
056import org.springframework.stereotype.Component;
057import org.springframework.util.StringUtils;
058
059/**
060 * Default implementation of {@link ViewService}.
061 *
062 * @author Richard "Shred" Körber
063 */
064@Component
065@ParametersAreNonnullByDefault
066public class ViewServiceImpl implements ViewService {
067    private final Logger log = LoggerFactory.getLogger(this.getClass());
068
069    @Resource private ViewManager viewManager;
070    @Resource private ServletContext servletContext;
071    @Resource private ConversionService conversionService;
072    @Resource private ApplicationContext appContext;
073
074    private Collection<ViewInterceptor> interceptors;
075
076    @PostConstruct
077    protected void setup() {
078        // Cannot immediately inject to the collection, as it fails when no
079        // ViewInterceptor bean was found.
080        interceptors = appContext.getBeansOfType(ViewInterceptor.class).values();
081    }
082
083    @Override
084    public void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws ViewException {
085        String path = req.getPathInfo();
086        if (path == null) {
087            path = "";
088        }
089
090        interceptors.forEach(it -> it.onRequest(req, resp));
091
092        String renderViewName = null;
093
094        try {
095            ViewContext context = getViewContext();
096            context.putTypedArgument(ServletContext.class, servletContext);
097            context.putTypedArgument(HttpServletResponse.class, resp);
098            renderViewName = invokeView(path);
099        } catch (ErrorResponseException ex) {
100            if (log.isDebugEnabled()) {
101                StringBuilder sb = new StringBuilder();
102                sb.append("View handler returned HTTP status ").append(ex.getResponseCode());
103                if (ex.getMessage() != null) {
104                    sb.append(" (").append(ex.getMessage()).append(')');
105                }
106                sb.append(" for path '").append(path).append('\'');
107                log.debug(sb.toString());
108            }
109
110            for (ViewInterceptor interceptor : interceptors) {
111                if (interceptor.onErrorResponse(ex, req, resp)) {
112                    return;
113                }
114            }
115
116            try {
117                if (ex.getMessage() != null) {
118                    resp.sendError(ex.getResponseCode(), ex.getMessage());
119                } else {
120                    resp.sendError(ex.getResponseCode());
121                }
122            } catch (IOException ex2) {
123                throw new ViewException("Failed to send error " + ex.getResponseCode(), ex2);
124            }
125
126            return;
127        }
128
129        if (renderViewName != null) {
130            for (ViewInterceptor interceptor : interceptors) {
131                String newViewName = interceptor.onRendering(renderViewName, req, resp);
132                if (newViewName != null) {
133                    renderViewName = newViewName;
134                }
135            }
136
137            try {
138                String fullViewPath = getTemplatePath(renderViewName);
139                RequestDispatcher dispatcher = servletContext.getRequestDispatcher(fullViewPath);
140                dispatcher.forward(req, resp);
141            } catch (IOException | ServletException ex) {
142                throw new ViewException("Failed to render " + renderViewName, ex);
143            }
144        }
145    }
146
147    @Override
148    public ViewContext getViewContext() {
149        return appContext.getBean("viewContext", ViewContext.class);
150    }
151
152    @Override
153    public String invokeView(String path) throws ViewException {
154        ViewContext context = getViewContext();
155
156        for (ViewPattern pattern : viewManager.getViewPatterns()) {
157            Map<String, String> pathParts = pattern.resolve(path);
158            if (pathParts != null) { // matched!
159                context.setPathParts(pathParts);
160                context.setQualifier(pattern.getQualifier());
161
162                ViewInvoker invoker = pattern.getInvoker();
163
164                interceptors.forEach(interceptor ->
165                    interceptor.onViewHandlerInvocation(context, invoker.getBean(), invoker.getMethod())
166                );
167
168                return pattern.getInvoker().invoke(context);
169            }
170        }
171
172        throw new PageNotFoundException("No page found at " + path);
173    }
174
175    @Override
176    public String buildPath(PathContext data, String view, PathType type) {
177        Collection<ViewPattern> vpList;
178
179        if (StringUtils.hasText(view)) {
180            // The given view is required...
181            vpList = viewManager.getViewPatternsForView(view, data.getQualifier());
182            if (vpList.isEmpty()) {
183                throw new IllegalArgumentException("Unknown view " + view);
184            }
185
186        } else {
187            // Find a view by the signature...
188            ViewPattern pattern = viewManager.getViewPatternForSignature(data.getSignature(), data.getQualifier());
189            if (pattern == null) {
190                throw new IllegalArgumentException("No view for signature: " + data.getSignature());
191            }
192            vpList = Collections.singletonList(pattern);
193        }
194
195        EvaluationContext evContext = createEvaluationContext(data);
196
197        for (ViewPattern pattern : vpList) {
198            String path = pattern.evaluate(evContext, data);
199            if (path != null) {
200                return processPath(path, type);
201            }
202        }
203
204        return null;
205    }
206
207    @Override
208    public String getTemplatePath(String template) {
209        if (template == null || template.isEmpty()) {
210            throw new IllegalArgumentException("template name not set");
211        }
212
213        String fullPath = template;
214        if (fullPath.startsWith("/")) {
215            fullPath = fullPath.substring(1);
216        }
217
218        return servletContext.getAttribute("jspPath") + fullPath;
219    }
220
221    /**
222     * Creates an {@link EvaluationContext} to be used for evaluation in this view
223     * service. The default implementation creates a {@link ViewPathEvaluationContext}.
224     *
225     * @param context
226     *            {@link PathContext} to be used as root object
227     * @return {@link EvaluationContext} to be used for evaluation
228     */
229    protected @Nonnull EvaluationContext createEvaluationContext(PathContext context) {
230        ViewPathEvaluationContext evContext = new ViewPathEvaluationContext(context);
231        evContext.setTypeConverter(new StandardTypeConverter(conversionService));
232        return evContext;
233    }
234
235    /**
236     * Processes a path, prefixing the servlet name and making it absolute if requested.
237     *
238     * @param path
239     *            relative path to be processed
240     * @param type
241     *            {@link PathType} to be returned
242     * @return URL to this path
243     */
244    private @Nonnull String processPath(String path, PathType type) {
245        if (type == PathType.VIEW) {
246            return path;
247        }
248
249        StringBuilder sb = new StringBuilder();
250
251        if (type == PathType.ABSOLUTE) {
252            sb.append(getViewContext().getRequestServerUrl());
253        }
254
255        sb.append(servletContext.getContextPath());
256        sb.append(getViewContext().getRequestServletName());
257        sb.append(path);
258
259        return sb.toString();
260    }
261
262}