001// Copyright 2021 The Apache Software Foundation
002//
003// Licensed under the Apache License, Version 2.0 (the "License");
004// you may not use this file except in compliance with the License.
005// You may obtain a copy of the License at
006//
007//     http://www.apache.org/licenses/LICENSE-2.0
008//
009// Unless required by applicable law or agreed to in writing, software
010// distributed under the License is distributed on an "AS IS" BASIS,
011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012// See the License for the specific language governing permissions and
013// limitations under the License.
014
015package org.apache.tapestry5.services;
016
017import java.util.Collections;
018import java.util.HashMap;
019import java.util.Map;
020import java.util.Objects;
021
022import javax.servlet.http.HttpServletResponse;
023
024import org.apache.tapestry5.StreamResponse;
025import org.apache.tapestry5.http.Link;
026
027/**
028 * <p>
029 * An event handler method may return an instance of this class to send an specific HTTP status
030 * code to the client. It also supports providing a string to be used as the response body
031 * and extra HTTP headers to be set. This class also provides some utility
032 * static methods for creating instances for specific HTTP statuses and a fluent API for setting
033 * additional information on them.
034 * </p>
035 * 
036 * <p>
037 * For returning binary content and/or adding a response header more than once and/or
038 * adding a response header without overwriting existing ones, implementing a {@link StreamResponse} 
039 * is the most probable better choice.
040 * </p>
041 *
042 * @since 5.8.0
043 */
044public final class HttpStatus
045{
046    private static final String CONTENT_LOCATION_HTTP_HEADER = "Content-Location";
047    
048    private static final String LOCATION_HTTP_HEADER = "Location";
049
050    private final int statusCode;
051
052    private String responseBody;
053    
054    private String contentType;
055    
056    private Map<String, String> extraHttpHeaders;
057    
058    /**
059     * Creates an instance with status code <code>200 OK</code>.
060     */
061    public static HttpStatus ok()
062    {
063        return new HttpStatus(HttpServletResponse.SC_OK);
064    }
065
066    /**
067     * Creates an instance with status code <code>201 Created</code>.
068     */
069    public static HttpStatus created()
070    {
071        return new HttpStatus(HttpServletResponse.SC_CREATED);
072    }
073
074    /**
075     * Creates an instance with status code <code>202 Accepted</code>.
076     */
077    public static HttpStatus accepted()
078    {
079        return new HttpStatus(HttpServletResponse.SC_ACCEPTED);
080    }
081    
082    /**
083     * Creates an instance with status code <code>404 Not Found</code>.
084     */
085    public static HttpStatus notFound()
086    {
087        return new HttpStatus(HttpServletResponse.SC_NOT_FOUND);
088    }
089    
090    /**
091     * Creates an instance with status code <code>403 Forbidden</code>.
092     */
093    public static HttpStatus forbidden()
094    {
095        return new HttpStatus(HttpServletResponse.SC_FORBIDDEN);
096    }
097    
098    /**
099     * Creates an instance with status code <code>400 Bad Request</code>.
100     */
101    public static HttpStatus badRequest()
102    {
103        return new HttpStatus(HttpServletResponse.SC_BAD_REQUEST);
104    }
105    
106    /**
107     * Creates an instance with status code <code>401 Unauthorized</code>.
108     */
109    public static HttpStatus unauthorized()
110    {
111        return new HttpStatus(HttpServletResponse.SC_UNAUTHORIZED);
112    }
113    
114    /**
115     * Creates an instance with status code <code>303 See Other</code>.
116     * @param location the value of the <code>Location</code> header.
117     */
118    public static HttpStatus seeOther(String location)
119    {
120        return new HttpStatus(HttpServletResponse.SC_SEE_OTHER).withLocation(location);
121    }
122    
123    /**
124     * Creates an instance with status code <code>303 See Also</code>.
125     * @param location the value of the <code>Location</code> header.
126     */
127    public static HttpStatus seeOther(Link location)
128    {
129        return new HttpStatus(HttpServletResponse.SC_SEE_OTHER).withLocation(location);
130    }
131    
132    /**
133     * Creates an instance with status code <code>301 Moved Permanently</code>.
134     * @param location the value of the <code>Location</code> header.
135     */
136    public static HttpStatus movedPermanently(String location)
137    {
138        return new HttpStatus(HttpServletResponse.SC_MOVED_PERMANENTLY).withLocation(location);
139    }
140    
141    /**
142     * Creates an instance with status code <code>301 Moved Permanently</code>.
143     * @param link the value of the <code>Location</code> header.
144     */
145    public static HttpStatus movedPermanently(Link link)
146    {
147        return movedPermanently(link.toRedirectURI());
148    }
149    
150    /**
151     * Creates an instance with status code <code>302 Found</code>.
152     * @param location the value of the <code>Location</code> header.
153     */
154    public static HttpStatus temporaryRedirect(String location)
155    {
156        return new HttpStatus(HttpServletResponse.SC_FOUND).withLocation(location);
157    }
158    
159    /**
160     * Creates an instance with status code <code>302 Found</code>.
161     * @param location the value of the <code>Location</code> header.
162     */
163    public static HttpStatus temporaryRedirect(Link location)
164    {
165        return temporaryRedirect(location.toRedirectURI());
166    }
167
168    /**
169     * Creates an object with a given status code and no response body.
170     */
171    public HttpStatus(int statusCode)
172    {
173        this(statusCode, null, null);
174    }
175    
176    /**
177     * Creates an object with a given status code, response body and <code>text/plain</code> MIME content type.
178     */
179    public HttpStatus(int statusCode, String responseBody)
180    {
181        this(statusCode, responseBody, "text/plain");
182    }
183
184    /**
185     * Creates an object with a given status code, response body and MIME content type.
186     */
187    public HttpStatus(int statusCode, String responseBody, String contentType)
188    {
189        this.statusCode = statusCode;
190        this.responseBody = responseBody;
191        this.contentType = contentType;
192    }
193    
194    /**
195     * Sets a redirect by using the <code>Location</code> HTTP header.
196     */
197    public HttpStatus withLocation(Link location)
198    {
199        return withLocation(location.toRedirectURI());
200    }
201    
202    /**
203     * Sets a redirect by using the <code>Location</code> HTTP header.
204     */
205    public HttpStatus withLocation(String location)
206    {
207        return withHttpHeader(LOCATION_HTTP_HEADER, location);
208    }
209
210    /**
211     * Sets the <code>Content-Location</code> HTTP header.
212     */
213    public HttpStatus withContentLocation(String location)
214    {
215        return withHttpHeader(CONTENT_LOCATION_HTTP_HEADER, location);
216    }
217    
218    /**
219     * Sets the <code>Content-Location</code> HTTP header.
220     */
221    public HttpStatus withContentLocation(Link link)
222    {
223        return withHttpHeader(CONTENT_LOCATION_HTTP_HEADER, link.toRedirectURI());
224    }
225    
226    /**
227     * Sets an HTTP header. If an existing value for this header already exists,
228     * it gets overwritten. If you need to set multiple headers or add them without
229     * overwriting existing ones, you need to implement {@link StreamResponse} instead.
230     */
231    public HttpStatus withHttpHeader(String name, String value)
232    {
233        Objects.requireNonNull(name, "Parameter name cannot be null");
234        Objects.requireNonNull(value, "Parameter value cannot be null");
235        if (extraHttpHeaders == null)
236        {
237            extraHttpHeaders = new HashMap<>(3);
238        }
239        extraHttpHeaders.put(name, value);
240        return this;
241    }
242
243    /**
244     * Returns the status code.
245     */
246    public int getStatusCode()
247    {
248        return statusCode;
249    }
250
251    /**
252     * Returns the response body.
253     */
254    public String getResponseBody()
255    {
256        return responseBody;
257    }
258
259    /**
260     * Returns the MIME content type of the response body.
261     */
262    public String getContentType() 
263    {
264        return contentType;
265    }
266
267    /**
268     * Returns the extra HTTP headers.
269     */
270    @SuppressWarnings("unchecked")
271    public Map<String, String> getExtraHttpHeaders() {
272        return extraHttpHeaders != null ? extraHttpHeaders : Collections.EMPTY_MAP;
273    }
274
275}