001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. 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 distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.rest; 014 015import static org.apache.juneau.internal.CollectionUtils.*; 016 017import java.io.*; 018import java.util.*; 019 020import jakarta.servlet.http.*; 021 022import org.apache.http.*; 023import org.apache.juneau.*; 024import org.apache.juneau.cp.*; 025import org.apache.juneau.http.response.*; 026import org.apache.juneau.rest.annotation.*; 027import org.apache.juneau.rest.logger.*; 028import org.apache.juneau.rest.util.*; 029 030/** 031 * Represents a single HTTP request. 032 * 033 * <h5 class='section'>Notes:</h5><ul> 034 * <li class='warn'>This class is not thread safe. 035 * </ul> 036 * 037 * <h5 class='section'>See Also:</h5><ul> 038 * </ul> 039 */ 040public class RestSession extends ContextSession { 041 042 //----------------------------------------------------------------------------------------------------------------- 043 // Static 044 //----------------------------------------------------------------------------------------------------------------- 045 046 /** 047 * Request attribute name for passing path variables from parent to child. 048 */ 049 private static final String REST_PATHVARS_ATTR = "juneau.pathVars"; 050 051 /** 052 * Creates a builder of this object. 053 * 054 * @param ctx The context creating this builder. 055 * @return A new builder. 056 */ 057 public static Builder create(RestContext ctx) { 058 return new Builder(ctx); 059 } 060 061 //----------------------------------------------------------------------------------------------------------------- 062 // Builder 063 //----------------------------------------------------------------------------------------------------------------- 064 065 /** 066 * Builder class. 067 */ 068 public static class Builder extends ContextSession.Builder { 069 070 RestContext ctx; 071 Object resource; 072 HttpServletRequest req; 073 HttpServletResponse res; 074 CallLogger logger; 075 String pathInfoUndecoded; 076 UrlPath urlPath; 077 078 /** 079 * Constructor. 080 * 081 * @param ctx The context creating this session. 082 */ 083 protected Builder(RestContext ctx) { 084 super(ctx); 085 this.ctx = ctx; 086 } 087 088 /** 089 * Specifies the servlet implementation bean. 090 * 091 * @param value The value for this setting. 092 * @return This object. 093 */ 094 public Builder resource(Object value) { 095 resource = value; 096 return this; 097 } 098 099 /** 100 * Specifies the HTTP servlet request object on this call. 101 * 102 * @param value The value for this setting. 103 * @return This object. 104 */ 105 public Builder req(HttpServletRequest value) { 106 req = value; 107 return this; 108 } 109 110 /** 111 * Returns the HTTP servlet request object on this call. 112 * 113 * @return The HTTP servlet request object on this call. 114 */ 115 public HttpServletRequest req() { 116 urlPath = null; 117 pathInfoUndecoded = null; 118 return req; 119 } 120 121 /** 122 * Specifies the HTTP servlet response object on this call. 123 * 124 * @param value The value for this setting. 125 * @return This object. 126 */ 127 public Builder res(HttpServletResponse value) { 128 res = value; 129 return this; 130 } 131 132 /** 133 * Returns the HTTP servlet response object on this call. 134 * 135 * @return The HTTP servlet response object on this call. 136 */ 137 public HttpServletResponse res() { 138 return res; 139 } 140 141 /** 142 * Specifies the logger to use for this session. 143 * 144 * @param value The value for this setting. 145 * @return This object. 146 */ 147 public Builder logger(CallLogger value) { 148 logger = value; 149 return this; 150 } 151 152 @Override /* Session.Builder */ 153 public RestSession build() { 154 return new RestSession(this); 155 } 156 157 /** 158 * Returns the request path info as a {@link UrlPath} bean. 159 * 160 * @return The request path info as a {@link UrlPath} bean. 161 */ 162 public UrlPath getUrlPath() { 163 if (urlPath == null) 164 urlPath = UrlPath.of(getPathInfoUndecoded()); 165 return urlPath; 166 } 167 168 /** 169 * Returns the request path info as a {@link UrlPath} bean. 170 * 171 * @return The request path info as a {@link UrlPath} bean. 172 */ 173 public String getPathInfoUndecoded() { 174 if (pathInfoUndecoded == null) 175 pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req); 176 return pathInfoUndecoded; 177 } 178 179 /** 180 * Adds resolved <c><ja>@Resource</ja>(path)</c> variable values to this call. 181 * 182 * @param value The variables to add to this call. 183 * @return This object. 184 */ 185 @SuppressWarnings("unchecked") 186 public Builder pathVars(Map<String,String> value) { 187 if (value != null && ! value.isEmpty()) { 188 Map<String,String> m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR); 189 if (m == null) { 190 m = new TreeMap<>(); 191 req.setAttribute(REST_PATHVARS_ATTR, m); 192 } 193 m.putAll(value); 194 } 195 return this; 196 } 197 } 198 199 //----------------------------------------------------------------------------------------------------------------- 200 // Static 201 //----------------------------------------------------------------------------------------------------------------- 202 203 private final Object resource; 204 private final RestContext context; 205 private HttpServletRequest req; 206 private HttpServletResponse res; 207 208 private CallLogger logger; 209 private UrlPath urlPath; 210 private String pathInfoUndecoded; 211 private long startTime = System.currentTimeMillis(); 212 private BeanStore beanStore; 213 private Map<String,String[]> queryParams; 214 private String method; 215 private RestOpSession opSession; 216 217 private UrlPathMatch urlPathMatch; 218 219 /** 220 * Constructor. 221 * 222 * @param builder The builder for this object. 223 */ 224 public RestSession(Builder builder) { 225 super(builder); 226 context = builder.ctx; 227 resource = builder.resource; 228 beanStore = BeanStore.of(context.getBeanStore(), resource).addBean(RestContext.class, context); 229 230 req = beanStore.add(HttpServletRequest.class, builder.req); 231 res = beanStore.add(HttpServletResponse.class, builder.res); 232 logger = beanStore.add(CallLogger.class, builder.logger); 233 urlPath = beanStore.add(UrlPath.class, builder.urlPath); 234 pathInfoUndecoded = builder.pathInfoUndecoded; 235 } 236 237 //------------------------------------------------------------------------------------------------------------------ 238 // Fluent setters. 239 //------------------------------------------------------------------------------------------------------------------ 240 241 /** 242 * Sets the logger to use when logging this call. 243 * 244 * @param value The new value for this setting. Can be <jk>null</jk>. 245 * @return This object. 246 */ 247 public RestSession logger(CallLogger value) { 248 logger = beanStore.add(CallLogger.class, value); 249 return this; 250 } 251 252 /** 253 * Enables or disabled debug mode on this call. 254 * 255 * @param value The new value for this setting. 256 * @return This object. 257 * @throws IOException Occurs if request content could not be cached into memory. 258 */ 259 public RestSession debug(boolean value) throws IOException { 260 if (value) { 261 req = CachingHttpServletRequest.wrap(req); 262 res = CachingHttpServletResponse.wrap(res); 263 req.setAttribute("Debug", true); 264 } else { 265 req.removeAttribute("Debug"); 266 } 267 return this; 268 } 269 270 /** 271 * Sets the HTTP status on this call. 272 * 273 * @param value The status code. 274 * @return This object. 275 */ 276 public RestSession status(int value) { 277 res.setStatus(value); 278 return this; 279 } 280 281 /** 282 * Sets the HTTP status on this call. 283 * 284 * @param value The status code. 285 * @return This object. 286 */ 287 public RestSession status(StatusLine value) { 288 if (value != null) 289 res.setStatus(value.getStatusCode()); 290 return this; 291 } 292 293 /** 294 * Identifies that an exception occurred during this call. 295 * 296 * @param value The thrown exception. 297 * @return This object. 298 */ 299 public RestSession exception(Throwable value) { 300 req.setAttribute("Exception", value); 301 beanStore.addBean(Throwable.class, value); 302 return this; 303 } 304 305 /** 306 * Sets the URL path pattern match on this call. 307 * 308 * @param value The match pattern. 309 * @return This object. 310 */ 311 public RestSession urlPathMatch(UrlPathMatch value) { 312 urlPathMatch = beanStore.add(UrlPathMatch.class, value); 313 return this; 314 } 315 316 //------------------------------------------------------------------------------------------------------------------ 317 // Getters 318 //------------------------------------------------------------------------------------------------------------------ 319 320 /** 321 * Returns the HTTP servlet request of this REST call. 322 * 323 * @return the HTTP servlet request of this REST call. 324 */ 325 public HttpServletRequest getRequest() { 326 return req; 327 } 328 329 /** 330 * Returns the HTTP servlet response of this REST call. 331 * 332 * @return the HTTP servlet response of this REST call. 333 */ 334 public HttpServletResponse getResponse() { 335 return res; 336 } 337 338 /** 339 * Returns the bean store of this call. 340 * 341 * @return The bean store of this call. 342 */ 343 public BeanStore getBeanStore() { 344 return beanStore; 345 } 346 347 /** 348 * Returns resolved <c><ja>@Resource</ja>(path)</c> variable values on this call. 349 * 350 * @return Resolved <c><ja>@Resource</ja>(path)</c> variable values on this call. 351 */ 352 @SuppressWarnings("unchecked") 353 public Map<String,String> getPathVars() { 354 Map<String,String> m = (Map<String,String>)req.getAttribute(REST_PATHVARS_ATTR); 355 return m == null ? Collections.emptyMap() : m; 356 } 357 358 /** 359 * Returns the URL path pattern match on this call. 360 * 361 * @return The URL path pattern match on this call. 362 */ 363 public UrlPathMatch getUrlPathMatch() { 364 return urlPathMatch; 365 } 366 367 /** 368 * Returns the exception that occurred during this call. 369 * 370 * @return The exception that occurred during this call. 371 */ 372 public Throwable getException() { 373 return (Throwable)req.getAttribute("Exception"); 374 } 375 376 //------------------------------------------------------------------------------------------------------------------ 377 // Lifecycle methods. 378 //------------------------------------------------------------------------------------------------------------------ 379 380 /** 381 * Called at the end of a call to finish any remaining tasks such as flushing buffers and logging the response. 382 * 383 * @return This object. 384 */ 385 public RestSession finish() { 386 try { 387 req.setAttribute("ExecTime", System.currentTimeMillis() - startTime); 388 if (opSession != null) 389 opSession.finish(); 390 else { 391 res.flushBuffer(); 392 } 393 } catch (Exception e) { 394 exception(e); 395 } 396 if (logger != null) 397 logger.log(req, res); 398 return this; 399 } 400 401 //------------------------------------------------------------------------------------------------------------------ 402 // Pass-through convenience methods. 403 //------------------------------------------------------------------------------------------------------------------ 404 405 /** 406 * Shortcut for calling <c>getRequest().getServletPath()</c>. 407 * 408 * @return The request servlet path. 409 */ 410 public String getServletPath() { 411 return req.getServletPath(); 412 } 413 414 /** 415 * Returns the request path info as a {@link UrlPath} bean. 416 * 417 * @return The request path info as a {@link UrlPath} bean. 418 */ 419 public UrlPath getUrlPath() { 420 if (urlPath == null) 421 urlPath = UrlPath.of(getPathInfoUndecoded()); 422 return urlPath; 423 } 424 425 /** 426 * Shortcut for calling <c>getRequest().getPathInfo()</c>. 427 * 428 * @return The request servlet path info. 429 */ 430 public String getPathInfo() { 431 return req.getPathInfo(); 432 } 433 434 /** 435 * Same as {@link #getPathInfo()} but doesn't decode encoded characters. 436 * 437 * @return The undecoded request servlet path info. 438 */ 439 public String getPathInfoUndecoded() { 440 if (pathInfoUndecoded == null) 441 pathInfoUndecoded = RestUtils.getPathInfoUndecoded(req); 442 return pathInfoUndecoded; 443 } 444 445 /** 446 * Returns the query parameters on the request. 447 * 448 * <p> 449 * Unlike {@link HttpServletRequest#getParameterMap()}, this doesn't parse the content if it's a POST. 450 * 451 * @return The query parameters on the request. 452 */ 453 public Map<String,String[]> getQueryParams() { 454 if (queryParams == null) { 455 if (req.getMethod().equalsIgnoreCase("POST")) 456 queryParams = RestUtils.parseQuery(req.getQueryString(), map()); 457 else 458 queryParams = req.getParameterMap(); 459 } 460 return queryParams; 461 } 462 463 /** 464 * Returns the HTTP method name. 465 * 466 * @return The HTTP method name, always uppercased. 467 */ 468 public String getMethod() { 469 if (method == null) { 470 471 Set<String> s1 = context.getAllowedMethodParams(); 472 Set<String> s2 = context.getAllowedMethodHeaders(); 473 474 if (! s1.isEmpty()) { 475 String[] x = getQueryParams().get("method"); 476 if (x != null && (s1.contains("*") || s1.contains(x[0]))) 477 method = x[0]; 478 } 479 480 if (method == null && ! s2.isEmpty()) { 481 String x = req.getHeader("X-Method"); 482 if (x != null && (s2.contains("*") || s2.contains(x))) 483 method = x; 484 } 485 486 if (method == null) 487 method = req.getMethod(); 488 489 method = method.toUpperCase(Locale.ENGLISH); 490 } 491 492 return method; 493 } 494 495 /** 496 * Shortcut for calling <c>getRequest().getStatus()</c>. 497 * 498 * @return The response status code. 499 */ 500 public int getStatus() { 501 return res.getStatus(); 502 } 503 504 /** 505 * Returns the context that created this call. 506 * 507 * @return The context that created this call. 508 */ 509 @Override 510 public RestContext getContext() { 511 return context; 512 } 513 514 /** 515 * Returns the REST object. 516 * 517 * @return The rest object. 518 */ 519 public Object getResource() { 520 return resource; 521 } 522 523 /** 524 * Returns the operation session of this REST session. 525 * 526 * <p> 527 * The operation session is created once the Java method to be invoked has been determined. 528 * 529 * @return The operation session of this REST session. 530 * @throws InternalServerError If operation session has not been created yet. 531 */ 532 public RestOpSession getOpSession() throws InternalServerError { 533 if (opSession == null) 534 throw new InternalServerError("Op Session not created."); 535 return opSession; 536 } 537 538 /** 539 * Runs this session. 540 * 541 * <p> 542 * Does the following: 543 * <ol> 544 * <li>Finds the Java method to invoke and creates a {@link RestOpSession} for it. 545 * <li>Invokes {@link RestPreCall} methods by calling {@link RestContext#preCall(RestOpSession)}. 546 * <li>Invokes Java method by calling {@link RestOpSession#run()}. 547 * <li>Invokes {@link RestPostCall} methods by calling {@link RestContext#postCall(RestOpSession)}. 548 * <li>If the Java method produced output, finds the response processor for it and runs it by calling {@link RestContext#processResponse(RestOpSession)}. 549 * <li>If no Java method matched, generates a 404/405/412 by calling {@link RestContext#handleNotFound(RestSession)}. 550 * </ol> 551 * 552 * @throws Throwable Any throwable can be thrown. 553 */ 554 public void run() throws Throwable { 555 try { 556 opSession = context.getRestOperations().findOperation(this).createSession(this).build(); 557 context.preCall(opSession); 558 opSession.run(); 559 context.postCall(opSession); 560 if (res.getStatus() == 0) 561 res.setStatus(200); 562 if (opSession.getResponse().hasContent()) { 563 // Now serialize the output if there was any. 564 // Some subclasses may write to the OutputStream or Writer directly. 565 context.processResponse(opSession); 566 } 567 } catch (NotFound e) { 568 if (getStatus() == 0) 569 status(404); 570 exception(e); 571 context.handleNotFound(this); 572 } 573 } 574}