001/*
002 * Copyright 2023 Web-Legacy
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.apache.tiles.request.jakarta.servlet;
017
018import java.io.IOException;
019import java.io.OutputStream;
020import java.io.PrintWriter;
021import java.io.Writer;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027
028import org.apache.tiles.request.AbstractClientRequest;
029import org.apache.tiles.request.ApplicationContext;
030import org.apache.tiles.request.attribute.Addable;
031import org.apache.tiles.request.collection.HeaderValuesMap;
032import org.apache.tiles.request.collection.ReadOnlyEnumerationMap;
033import org.apache.tiles.request.collection.ScopeMap;
034import org.apache.tiles.request.jakarta.servlet.extractor.HeaderExtractor;
035import org.apache.tiles.request.jakarta.servlet.extractor.ParameterExtractor;
036import org.apache.tiles.request.jakarta.servlet.extractor.RequestScopeExtractor;
037import org.apache.tiles.request.jakarta.servlet.extractor.SessionScopeExtractor;
038
039import jakarta.servlet.RequestDispatcher;
040import jakarta.servlet.ServletException;
041import jakarta.servlet.ServletResponse;
042import jakarta.servlet.http.HttpServletRequest;
043import jakarta.servlet.http.HttpServletResponse;
044
045/**
046 * Servlet-based implementation of the TilesApplicationContext interface.
047 *
048 * <p>Copied from Apache tiles-request-servlet 1.0.7 and adapted for
049 * Jakarta EE 9.</p>
050 */
051public class ServletRequest extends AbstractClientRequest {
052
053    /**
054     * The native available scopes: request, session and application.
055     */
056    private static final List<String> SCOPES
057            = Collections.unmodifiableList(Arrays.asList(
058                    REQUEST_SCOPE, "session", APPLICATION_SCOPE));
059
060    /**
061     * The request object to use.
062     */
063    private HttpServletRequest request;
064
065    /**
066     * The response object to use.
067     */
068    private HttpServletResponse response;
069
070    /**
071     * The response output stream, lazily initialized.
072     */
073    private OutputStream outputStream;
074
075    /**
076     * The response writer, lazily initialized.
077     */
078    private PrintWriter writer;
079
080    /**
081     * The lazily instantiated {@code Map} of header name-value combinations
082     * (immutable).
083     */
084    private Map<String, String> header = null;
085
086    /**
087     * The lazily instantiated {@code Map} of header name-value combinations
088     * (write-only).
089     */
090    private Addable<String> responseHeaders = null;
091
092    /**
093     * The lazily instantiated {@code Map} of header name-values combinations
094     * (immutable).
095     */
096    private Map<String, String[]> headerValues = null;
097
098    /**
099     * 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}