1 /*
2 * Copyright 2023 Web-Legacy
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.apache.tiles.request.jakarta.servlet;
17
18 import java.io.IOException;
19 import java.io.OutputStream;
20 import java.io.PrintWriter;
21 import java.io.Writer;
22 import java.util.Arrays;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Map;
27
28 import org.apache.tiles.request.AbstractClientRequest;
29 import org.apache.tiles.request.ApplicationContext;
30 import org.apache.tiles.request.attribute.Addable;
31 import org.apache.tiles.request.collection.HeaderValuesMap;
32 import org.apache.tiles.request.collection.ReadOnlyEnumerationMap;
33 import org.apache.tiles.request.collection.ScopeMap;
34 import org.apache.tiles.request.jakarta.servlet.extractor.HeaderExtractor;
35 import org.apache.tiles.request.jakarta.servlet.extractor.ParameterExtractor;
36 import org.apache.tiles.request.jakarta.servlet.extractor.RequestScopeExtractor;
37 import org.apache.tiles.request.jakarta.servlet.extractor.SessionScopeExtractor;
38
39 import jakarta.servlet.RequestDispatcher;
40 import jakarta.servlet.ServletException;
41 import jakarta.servlet.ServletResponse;
42 import jakarta.servlet.http.HttpServletRequest;
43 import jakarta.servlet.http.HttpServletResponse;
44
45 /**
46 * Servlet-based implementation of the TilesApplicationContext interface.
47 *
48 * <p>Copied from Apache tiles-request-servlet 1.0.7 and adapted for
49 * Jakarta EE 9.</p>
50 */
51 public class ServletRequest extends AbstractClientRequest {
52
53 /**
54 * The native available scopes: request, session and application.
55 */
56 private static final List<String> SCOPES
57 = Collections.unmodifiableList(Arrays.asList(
58 REQUEST_SCOPE, "session", APPLICATION_SCOPE));
59
60 /**
61 * The request object to use.
62 */
63 private HttpServletRequest request;
64
65 /**
66 * The response object to use.
67 */
68 private HttpServletResponse response;
69
70 /**
71 * The response output stream, lazily initialized.
72 */
73 private OutputStream outputStream;
74
75 /**
76 * The response writer, lazily initialized.
77 */
78 private PrintWriter writer;
79
80 /**
81 * The lazily instantiated {@code Map} of header name-value combinations
82 * (immutable).
83 */
84 private Map<String, String> header = null;
85
86 /**
87 * The lazily instantiated {@code Map} of header name-value combinations
88 * (write-only).
89 */
90 private Addable<String> responseHeaders = null;
91
92 /**
93 * The lazily instantiated {@code Map} of header name-values combinations
94 * (immutable).
95 */
96 private Map<String, String[]> headerValues = null;
97
98 /**
99 * The lazily instantiated {@code Map} of request parameter name-value.
100 */
101 private Map<String, String> param = null;
102
103 /**
104 * The lazily instantiated {@code Map} of request scope attributes.
105 */
106 private Map<String, Object> requestScope = null;
107
108 /**
109 * The lazily instantiated {@code Map} of session scope attributes.
110 */
111 private Map<String, Object> sessionScope = null;
112
113 /**
114 * Creates a new instance of ServletTilesRequestContext.
115 *
116 * @param applicationContext The application context.
117 * @param request The request object.
118 * @param response The response object.
119 */
120 public ServletRequest(
121 ApplicationContext applicationContext,
122 HttpServletRequest request, HttpServletResponse response) {
123
124 super(applicationContext);
125 this.request = request;
126 this.response = response;
127 }
128
129 /**
130 * Return an immutable Map that maps header names to the first (or only)
131 * header value (as a String).
132 *
133 * @return The header map.
134 */
135 @Override
136 public Map<String, String> getHeader() {
137 if (header == null && request != null) {
138 header = new ReadOnlyEnumerationMap<String>(
139 new HeaderExtractor(request, null));
140 }
141
142 return header;
143 }
144
145 /**
146 * Return an add-able object that can be used to write headers to the
147 * response.
148 *
149 * @return An add-able object.
150 */
151 @Override
152 public Addable<String> getResponseHeaders() {
153 if (responseHeaders == null && response != null) {
154 responseHeaders = new HeaderExtractor(null, response);
155 }
156
157 return responseHeaders;
158 }
159
160 /**
161 * Return an immutable Map that maps header names to the set of all values
162 * specified in the request (as a String array). Header names must be
163 * matched in a case-insensitive manner.
164 *
165 * @return The header values map.
166 */
167 @Override
168 public Map<String, String[]> getHeaderValues() {
169 if (headerValues == null && request != null) {
170 headerValues = new HeaderValuesMap(
171 new HeaderExtractor(request, response));
172 }
173
174 return headerValues;
175 }
176
177 /**
178 * Return an immutable Map that maps request parameter names to the first
179 * (or only) value (as a String).
180 *
181 * @return The parameter map.
182 */
183 @Override
184 public Map<String, String> getParam() {
185 if (param == null && request != null) {
186 param = new ReadOnlyEnumerationMap<String>(
187 new ParameterExtractor(request));
188 }
189
190 return param;
191 }
192
193 /**
194 * Return an immutable Map that maps request parameter names to the set of
195 * all values (as a String array).
196 *
197 * @return The parameter values map.
198 */
199 @Override
200 public Map<String, String[]> getParamValues() {
201 return request.getParameterMap();
202 }
203
204 /**
205 * Returns a context map, given the scope name.
206 *
207 * <p>This method always return a map for all the scope names returned by
208 * {@link #getAvailableScopes()}. That map may be writable, or immutable,
209 * depending on the implementation.
210 *
211 * @param scope The name of the scope.
212 *
213 * @return The context.
214 */
215 @Override
216 public Map<String, Object> getContext(String scope) {
217 if (REQUEST_SCOPE.equals(scope)) {
218 return getRequestScope();
219 } else if ("session".equals(scope)) {
220 return getSessionScope();
221 } else if (APPLICATION_SCOPE.equals(scope)) {
222 return getApplicationScope();
223 }
224
225 throw new IllegalArgumentException(scope + " does not exist. "
226 + "Call getAvailableScopes() first to check.");
227 }
228
229 /**
230 * Returns the context map from request scope.
231 *
232 * @return the context map from request scope
233 */
234 public Map<String, Object> getRequestScope() {
235 if (requestScope == null && request != null) {
236 requestScope = new ScopeMap(new RequestScopeExtractor(request));
237 }
238
239 return requestScope;
240 }
241
242 /**
243 * Returns the context map from session scope.
244 *
245 * @return the context map from session scope
246 */
247 public Map<String, Object> getSessionScope() {
248 if (sessionScope == null && request != null) {
249 sessionScope = new ScopeMap(new SessionScopeExtractor(request));
250 }
251
252 return sessionScope;
253 }
254
255 /**
256 * Returns all available scopes.
257 *
258 * <p>The scopes are ordered according to their lifetime, the innermost,
259 * shorter lived scope appears first, and the outermost, longer lived scope
260 * appears last. Besides, the scopes "request" and "application" always
261 * included in the list.</p>
262 *
263 * @return All the available scopes.
264 */
265 @Override
266 public List<String> getAvailableScopes() {
267 return SCOPES;
268 }
269
270 /**
271 * Forwards to a path.
272 *
273 * @param path The path to forward to.
274 *
275 * @throws IOException If something goes wrong when forwarding.
276 */
277 @Override
278 public void doForward(String path) throws IOException {
279 if (response.isCommitted()) {
280 doInclude(path);
281 } else {
282 forward(path);
283 }
284 }
285
286 /**
287 * Includes the content of a resource (servlet, JSP page, HTML file) in the
288 * response. In essence, this method enables programmatic server-side includes.
289 *
290 * @param path a {@code String} specifying the pathname to the resource. If
291 * it is relative, it must be relative against the current servlet.
292 *
293 * @throws IOException if the included resource throws this exception
294 *
295 * @see RequestDispatcher#include(jakarta.servlet.ServletRequest, ServletResponse)
296 */
297 public void doInclude(String path) throws IOException {
298 RequestDispatcher rd = request.getRequestDispatcher(path);
299
300 if (rd == null) {
301 throw new IOException("No request dispatcher returned for path '"
302 + path + "'");
303 }
304
305 try {
306 rd.include(request, response);
307 } catch (ServletException ex) {
308 throw ServletUtil.wrapServletException(
309 ex, "ServletException including path '" + path + "'.");
310 }
311 }
312
313 /**
314 * Forwards to a path.
315 *
316 * @param path The path to forward to.
317 *
318 * @throws IOException If something goes wrong during the operation.
319 */
320 private void forward(String path) throws IOException {
321 RequestDispatcher rd = request.getRequestDispatcher(path);
322
323 if (rd == null) {
324 throw new IOException("No request dispatcher returned for path '"
325 + path + "'");
326 }
327
328 try {
329 rd.forward(request, response);
330 } catch (ServletException ex) {
331 throw ServletUtil.wrapServletException(
332 ex, "ServletException including path '" + path + "'.");
333 }
334 }
335
336 /**
337 * Returns a {@link jakarta.servlet.ServletOutputStream} suitable for
338 * writing binary data in the response. The servlet container does not
339 * encode the binary data.
340 *
341 * @return a {@link jakarta.servlet.ServletOutputStream} for writing binary
342 * data
343 *
344 * @throws IllegalStateException if the {@code getWriter} method has
345 * been called on this response
346 * @throws IOException if an input or output exception occurred
347 *
348 * @see HttpServletResponse#getOutputStream()
349 */
350 public OutputStream getOutputStream() throws IOException {
351 if (outputStream == null) {
352 outputStream = response.getOutputStream();
353 }
354
355 return outputStream;
356 }
357
358 /**
359 * Returns a {@code Writer} object that can send character text to the
360 * client. The {@code Writer} uses the character encoding returned by
361 * {@link HttpServletResponse#getCharacterEncoding()}. If the response's
362 * character encoding has not been specified as described in
363 * {@code getCharacterEncoding} (i.e., the method just returns the default
364 * value {@code ISO-8859-1}), {@code getWriter} updates it to
365 * {@code ISO-8859-1}.
366 *
367 * @return a {@code Writer} object that can return character data to the
368 * client
369 *
370 * @throws java.io.UnsupportedEncodingException if the character encoding
371 * returned by {@code getCharacterEncoding}
372 * cannot be used
373 * @throws IllegalStateException if the {@code getOutputStream} method has
374 * already been called for this response
375 * object
376 * @throws IOException if an input or output exception occurred
377 *
378 * @see #getPrintWriter()
379 * @see HttpServletResponse#getWriter()
380 */
381 public Writer getWriter() throws IOException {
382 return getPrintWriter();
383 }
384
385 /**
386 * Returns a {@code PrintWriter} object that can send character text to the
387 * client. The {@code PrintWriter} uses the character encoding returned by
388 * {@link HttpServletResponse#getCharacterEncoding()}. If the response's
389 * character encoding has not been specified as described in
390 * {@code getCharacterEncoding} (i.e., the method just returns the default
391 * value {@code ISO-8859-1}), {@code getWriter} updates it to
392 * {@code ISO-8859-1}.
393 *
394 * @return a {@code PrintWriter} object that can return character data to
395 * the client
396 *
397 * @throws java.io.UnsupportedEncodingException if the character encoding
398 * returned by {@code getCharacterEncoding}
399 * cannot be used
400 * @throws IllegalStateException if the {@code getOutputStream} method has
401 * already been called for this response
402 * object
403 * @throws IOException if an input or output exception occurred
404 *
405 * @see HttpServletResponse#getWriter()
406 */
407 public PrintWriter getPrintWriter() throws IOException {
408 if (writer == null) {
409 writer = response.getWriter();
410 }
411
412 return writer;
413 }
414
415 /**
416 * Returns a boolean indicating if the response has been committed. A
417 * committed response has already had its status code and headers written.
418 *
419 * @return a boolean indicating if the response has been committed
420 *
421 * @see HttpServletResponse#isCommitted()
422 */
423 public boolean isResponseCommitted() {
424 return response.isCommitted();
425 }
426
427 /**
428 * Sets the content type of the response being sent to the client, if the
429 * response has not been committed yet. The given content type may include
430 * a character encoding specification, for example,
431 * {@code>text/html;charset=UTF-8}. The response's character encoding is
432 * only set from the given content type if this method is called before
433 * {@code getWriter} is called.
434 *
435 * @param contentType a {@code String} specifying the MIME type of the
436 * content
437 *
438 * @see HttpServletResponse#setContentType(String)
439 */
440 public void setContentType(String contentType) {
441 response.setContentType(contentType);
442 }
443
444 /**
445 * Returns the preferred {@code Locale} that the client will accept content
446 * in, based on the Accept-Language header. If the client request doesn't
447 * provide an Accept-Language header, this method returns the default
448 * locale for the server.
449 *
450 * @return the preferred {@code Locale} for the client
451 *
452 * @see HttpServletRequest#getLocale()
453 */
454 public Locale getRequestLocale() {
455 return request.getLocale();
456 }
457
458 /**
459 * Returns the request object to use.
460 *
461 * @return the request object to use
462 */
463 public HttpServletRequest getRequest() {
464 return request;
465 }
466
467 /**
468 * Returns the response object to use.
469 *
470 * @return the response object to use
471 */
472 public HttpServletResponse getResponse() {
473 return response;
474 }
475
476 /**
477 * Returns a boolean indicating whether the authenticated user is included
478 * in the specified logical "role". Roles and role membership can be
479 * defined using deployment descriptors. If the user has not been
480 * authenticated, the method returns {@code false}.
481 *
482 * @param role a {@code String} specifying the name of the role
483 *
484 * @return a {@code boolean} indicating whether the user making this
485 * request belongs to a given role; {@code false} if the user has
486 * not been authenticated
487 *
488 * @see HttpServletRequest#isUserInRole(String)
489 */
490 public boolean isUserInRole(String role) {
491 return request.isUserInRole(role);
492 }
493 }