Sat, 16 May 2020 16:14:10 +0200
adds missing spaces in error messages
1 /*
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3 *
4 * Copyright 2018 Mike Becker. All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
20 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26 * POSSIBILITY OF SUCH DAMAGE.
27 *
28 */
29 package de.uapcore.lightpit;
31 import de.uapcore.lightpit.dao.DataAccessObjects;
32 import de.uapcore.lightpit.dao.postgres.PGDataAccessObjects;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
36 import javax.servlet.ServletException;
37 import javax.servlet.http.HttpServlet;
38 import javax.servlet.http.HttpServletRequest;
39 import javax.servlet.http.HttpServletResponse;
40 import javax.servlet.http.HttpSession;
41 import java.io.IOException;
42 import java.lang.reflect.Constructor;
43 import java.lang.reflect.Method;
44 import java.lang.reflect.Modifier;
45 import java.sql.Connection;
46 import java.sql.SQLException;
47 import java.util.*;
49 /**
50 * A special implementation of a HTTPServlet which is focused on implementing
51 * the necessary functionality for {@link LightPITModule}s.
52 */
53 public abstract class AbstractLightPITServlet extends HttpServlet {
55 private static final Logger LOG = LoggerFactory.getLogger(AbstractLightPITServlet.class);
57 private static final String SITE_JSP = Functions.jspPath("site");
59 /**
60 * The EL proxy is necessary, because the EL resolver cannot handle annotation properties.
61 */
62 private LightPITModule.ELProxy moduleInfo = null;
64 /**
65 * Invocation mapping gathered from the {@link RequestMapping} annotations.
66 * <p>
67 * Paths in this map must always start with a leading slash, although
68 * the specification in the annotation must not start with a leading slash.
69 * <p>
70 * The reason for this is the different handling of empty paths in
71 * {@link HttpServletRequest#getPathInfo()}.
72 */
73 private final Map<HttpMethod, Map<String, Method>> mappings = new HashMap<>();
75 private final List<MenuEntry> subMenu = new ArrayList<>();
77 /**
78 * Gives implementing modules access to the {@link ModuleManager}.
79 *
80 * @return the module manager
81 */
82 protected final ModuleManager getModuleManager() {
83 return (ModuleManager) getServletContext().getAttribute(ModuleManager.SC_ATTR_NAME);
84 }
87 /**
88 * Creates a set of data access objects for the specified connection.
89 *
90 * @param connection the SQL connection
91 * @return a set of data access objects
92 */
93 private DataAccessObjects createDataAccessObjects(Connection connection) throws SQLException {
94 final var df = (DatabaseFacade) getServletContext().getAttribute(DatabaseFacade.SC_ATTR_NAME);
95 if (df.getSQLDialect() == DatabaseFacade.Dialect.Postgres) {
96 return new PGDataAccessObjects(connection);
97 }
98 throw new AssertionError("Non-exhaustive if-else - this is a bug.");
99 }
101 private ResponseType invokeMapping(Method method, HttpServletRequest req, HttpServletResponse resp, DataAccessObjects dao) throws IOException {
102 try {
103 LOG.trace("invoke {}#{}", method.getDeclaringClass().getName(), method.getName());
104 final var paramTypes = method.getParameterTypes();
105 final var paramValues = new Object[paramTypes.length];
106 for (int i = 0; i < paramTypes.length; i++) {
107 if (paramTypes[i].isAssignableFrom(HttpServletRequest.class)) {
108 paramValues[i] = req;
109 } else if (paramTypes[i].isAssignableFrom(HttpServletResponse.class)) {
110 paramValues[i] = resp;
111 }
112 if (paramTypes[i].isAssignableFrom(DataAccessObjects.class)) {
113 paramValues[i] = dao;
114 }
115 }
116 return (ResponseType) method.invoke(this, paramValues);
117 } catch (ReflectiveOperationException | ClassCastException ex) {
118 LOG.error("invocation of method {} failed: {}", method.getName(), ex.getMessage());
119 LOG.debug("Details: ", ex);
120 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
121 return ResponseType.NONE;
122 }
123 }
125 @Override
126 public void init() throws ServletException {
127 moduleInfo = Optional.ofNullable(this.getClass().getAnnotation(LightPITModule.class))
128 .map(LightPITModule.ELProxy::new).orElse(null);
130 if (moduleInfo != null) {
131 scanForRequestMappings();
132 }
134 LOG.trace("{} initialized", getServletName());
135 }
137 private void scanForRequestMappings() {
138 try {
139 Method[] methods = getClass().getDeclaredMethods();
140 for (Method method : methods) {
141 Optional<RequestMapping> mapping = Optional.ofNullable(method.getAnnotation(RequestMapping.class));
142 if (mapping.isPresent()) {
143 if (!Modifier.isPublic(method.getModifiers())) {
144 LOG.warn("{} is annotated with {} but is not public",
145 method.getName(), RequestMapping.class.getSimpleName()
146 );
147 continue;
148 }
149 if (Modifier.isAbstract(method.getModifiers())) {
150 LOG.warn("{} is annotated with {} but is abstract",
151 method.getName(), RequestMapping.class.getSimpleName()
152 );
153 continue;
154 }
155 if (!ResponseType.class.isAssignableFrom(method.getReturnType())) {
156 LOG.warn("{} is annotated with {} but has the wrong return type - 'ResponseType' required",
157 method.getName(), RequestMapping.class.getSimpleName()
158 );
159 continue;
160 }
162 boolean paramsInjectible = true;
163 for (var param : method.getParameterTypes()) {
164 paramsInjectible &= HttpServletRequest.class.isAssignableFrom(param)
165 || HttpServletResponse.class.isAssignableFrom(param)
166 || DataAccessObjects.class.isAssignableFrom(param);
167 }
168 if (paramsInjectible) {
169 final String requestPath = "/" + mapping.get().requestPath();
171 if (mappings
172 .computeIfAbsent(mapping.get().method(), k -> new HashMap<>())
173 .putIfAbsent(requestPath, method) != null) {
174 LOG.warn("{} {} has multiple mappings",
175 mapping.get().method(),
176 mapping.get().requestPath()
177 );
178 }
180 final var menuKey = mapping.get().menuKey();
181 if (!menuKey.isBlank()) {
182 subMenu.add(new MenuEntry(
183 new ResourceKey(moduleInfo.getBundleBaseName(), menuKey),
184 moduleInfo.getModulePath() + requestPath,
185 mapping.get().menuSequence()));
186 }
188 LOG.debug("{} {} maps to {}::{}",
189 mapping.get().method(),
190 requestPath,
191 getClass().getSimpleName(),
192 method.getName()
193 );
194 } else {
195 LOG.warn("{} is annotated with {} but has the wrong parameters - only HttpServletRequest. HttpServletResponse, and DataAccessObjects are allowed",
196 method.getName(), RequestMapping.class.getSimpleName()
197 );
198 }
199 }
200 }
201 } catch (SecurityException ex) {
202 LOG.error("Scan for request mappings on declared methods failed.", ex);
203 }
204 }
206 @Override
207 public void destroy() {
208 mappings.clear();
209 LOG.trace("{} destroyed", getServletName());
210 }
212 /**
213 * Sets the name of the dynamic fragment.
214 * <p>
215 * It is sufficient to specify the name without any extension. The extension
216 * is added automatically if not specified.
217 * <p>
218 * The fragment must be located in the dynamic fragments folder.
219 *
220 * @param req the servlet request object
221 * @param fragmentName the name of the fragment
222 * @see Constants#DYN_FRAGMENT_PATH_PREFIX
223 */
224 public void setDynamicFragment(HttpServletRequest req, String fragmentName) {
225 req.setAttribute(Constants.REQ_ATTR_FRAGMENT, Functions.dynFragmentPath(fragmentName));
226 }
228 /**
229 * @param req the servlet request object
230 * @param location the location where to redirect
231 * @see Constants#REQ_ATTR_REDIRECT_LOCATION
232 */
233 public void setRedirectLocation(HttpServletRequest req, String location) {
234 if (location.startsWith("./")) {
235 location = location.replaceFirst("\\./", Functions.baseHref(req));
236 }
237 req.setAttribute(Constants.REQ_ATTR_REDIRECT_LOCATION, location);
238 }
240 /**
241 * Specifies the name of an additional stylesheet used by the module.
242 * <p>
243 * Setting an additional stylesheet is optional, but quite common for HTML
244 * output.
245 * <p>
246 * It is sufficient to specify the name without any extension. The extension
247 * is added automatically if not specified.
248 *
249 * @param req the servlet request object
250 * @param stylesheet the name of the stylesheet
251 */
252 public void setStylesheet(HttpServletRequest req, String stylesheet) {
253 req.setAttribute(Constants.REQ_ATTR_STYLESHEET, Functions.enforceExt(stylesheet, ".css"));
254 }
256 /**
257 * Obtains a request parameter of the specified type.
258 * The specified type must have a single-argument constructor accepting a string to perform conversion.
259 * The constructor of the specified type may throw an exception on conversion failures.
260 *
261 * @param req the servlet request object
262 * @param clazz the class object of the expected type
263 * @param name the name of the parameter
264 * @param <T> the expected type
265 * @return the parameter value or an empty optional, if no parameter with the specified name was found
266 */
267 public<T> Optional<T> getParameter(HttpServletRequest req, Class<T> clazz, String name) {
268 final String paramValue = req.getParameter(name);
269 if (paramValue == null) return Optional.empty();
270 if (clazz.equals(String.class)) return Optional.of((T)paramValue);
271 try {
272 final Constructor<T> ctor = clazz.getConstructor(String.class);
273 return Optional.of(ctor.newInstance(paramValue));
274 } catch (ReflectiveOperationException e) {
275 throw new RuntimeException(e);
276 }
278 }
280 private void forwardToFullView(HttpServletRequest req, HttpServletResponse resp)
281 throws IOException, ServletException {
283 req.setAttribute(Constants.REQ_ATTR_MENU, getModuleManager().getMainMenu());
284 req.setAttribute(Constants.REQ_ATTR_SUB_MENU, subMenu);
285 req.getRequestDispatcher(SITE_JSP).forward(req, resp);
286 }
288 private String sanitizeRequestPath(HttpServletRequest req) {
289 return Optional.ofNullable(req.getPathInfo()).orElse("/");
290 }
292 private Optional<Method> findMapping(HttpMethod method, HttpServletRequest req) {
293 return Optional.ofNullable(mappings.get(method)).map(rm -> rm.get(sanitizeRequestPath(req)));
294 }
296 private void forwardAsSpecified(ResponseType type, HttpServletRequest req, HttpServletResponse resp)
297 throws ServletException, IOException {
298 switch (type) {
299 case NONE:
300 return;
301 case HTML:
302 forwardToFullView(req, resp);
303 return;
304 // TODO: implement remaining response types
305 default:
306 throw new AssertionError("ResponseType switch is not exhaustive - this is a bug!");
307 }
308 }
310 private void doProcess(HttpMethod method, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
312 // choose the requested language as session language (if available) or fall back to english, otherwise
313 HttpSession session = req.getSession();
314 if (session.getAttribute(Constants.SESSION_ATTR_LANGUAGE) == null) {
315 Optional<List<String>> availableLanguages = Functions.availableLanguages(getServletContext()).map(Arrays::asList);
316 Optional<Locale> reqLocale = Optional.of(req.getLocale());
317 Locale sessionLocale = reqLocale.filter((rl) -> availableLanguages.map((al) -> al.contains(rl.getLanguage())).orElse(false)).orElse(Locale.ENGLISH);
318 session.setAttribute(Constants.SESSION_ATTR_LANGUAGE, sessionLocale);
319 LOG.debug("Setting language for new session {}: {}", session.getId(), sessionLocale.getDisplayLanguage());
320 } else {
321 Locale sessionLocale = (Locale) session.getAttribute(Constants.SESSION_ATTR_LANGUAGE);
322 resp.setLocale(sessionLocale);
323 LOG.trace("Continuing session {} with language {}", session.getId(), sessionLocale);
324 }
326 // set some internal request attributes
327 final String fullPath = Functions.fullPath(req);
328 req.setAttribute(Constants.REQ_ATTR_BASE_HREF, Functions.baseHref(req));
329 req.setAttribute(Constants.REQ_ATTR_PATH, fullPath);
330 Optional.ofNullable(moduleInfo).ifPresent((proxy) -> req.setAttribute(Constants.REQ_ATTR_MODULE_INFO, proxy));
332 // if this is an error path, bypass the normal flow
333 if (fullPath.startsWith("/error/")) {
334 final var mapping = findMapping(method, req);
335 if (mapping.isPresent()) {
336 forwardAsSpecified(invokeMapping(mapping.get(), req, resp, null), req, resp);
337 }
338 return;
339 }
341 // obtain a connection and create the data access objects
342 final var db = (DatabaseFacade) req.getServletContext().getAttribute(DatabaseFacade.SC_ATTR_NAME);
343 final var ds = db.getDataSource();
344 if (ds == null) {
345 resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "JNDI DataSource lookup failed. See log for details.");
346 return;
347 }
348 try (final var connection = ds.getConnection()) {
349 final var dao = createDataAccessObjects(connection);
350 try {
351 connection.setAutoCommit(false);
352 // call the handler, if available, or send an HTTP 404 error
353 final var mapping = findMapping(method, req);
354 if (mapping.isPresent()) {
355 forwardAsSpecified(invokeMapping(mapping.get(), req, resp, dao), req, resp);
356 } else {
357 resp.sendError(HttpServletResponse.SC_NOT_FOUND);
358 }
359 connection.commit();
360 } catch (SQLException ex) {
361 LOG.warn("Database transaction failed (Code {}): {}", ex.getErrorCode(), ex.getMessage());
362 LOG.debug("Details: ", ex);
363 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unhandled Transaction Error - Code: " + ex.getErrorCode());
364 connection.rollback();
365 }
366 } catch (SQLException ex) {
367 LOG.error("Severe Database Exception (Code {}): {}", ex.getErrorCode(), ex.getMessage());
368 LOG.debug("Details: ", ex);
369 resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Database Error - Code: " + ex.getErrorCode());
370 }
371 }
373 @Override
374 protected final void doGet(HttpServletRequest req, HttpServletResponse resp)
375 throws ServletException, IOException {
376 doProcess(HttpMethod.GET, req, resp);
377 }
379 @Override
380 protected final void doPost(HttpServletRequest req, HttpServletResponse resp)
381 throws ServletException, IOException {
382 doProcess(HttpMethod.POST, req, resp);
383 }
384 }