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