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