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}