001/* 002 * Copyright 2011-2016 UnboundID Corp. 003 * 004 * This program is free software; you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License (GPLv2 only) 006 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 007 * as published by the Free Software Foundation. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program; if not, see <http://www.gnu.org/licenses>. 016 */ 017 018package com.unboundid.scim.sdk; 019 020import com.unboundid.scim.schema.ResourceDescriptor; 021 022import javax.servlet.http.HttpServletRequest; 023import javax.ws.rs.core.EntityTag; 024import javax.ws.rs.core.HttpHeaders; 025import java.net.URI; 026import java.util.ArrayList; 027import java.util.Collections; 028import java.util.List; 029 030 031/** 032 * This class is the base class for all SCIM requests. 033 */ 034public abstract class SCIMRequest 035{ 036 /** 037 * The base URL for the SCIM service. 038 */ 039 private final URI baseURL; 040 041 /** 042 * The authenticated user ID or {@code null} if the request is not 043 * authenticated. 044 */ 045 private final String authenticatedUserID; 046 047 /** 048 * The ResourceDescriptor associated with this request. 049 */ 050 private final ResourceDescriptor resourceDescriptor; 051 052 /** 053 * The HttpServletRequest that initiated this SCIM request. 054 */ 055 private final HttpServletRequest httpServletRequest; 056 057 private final String ifMatchHeaderValue; 058 059 private final String ifNoneMatchHeaderValue; 060 061 062 /** 063 * Create a new SCIM request from the provided information. 064 * 065 * @param baseURL The base URL for the SCIM service. 066 * @param authenticatedUserID The authenticated user name or {@code null} if 067 * the request is not authenticated. 068 * @param resourceDescriptor The ResourceDescriptor associated with this 069 * request. 070 */ 071 public SCIMRequest(final URI baseURL, final String authenticatedUserID, 072 final ResourceDescriptor resourceDescriptor) 073 { 074 this.baseURL = baseURL; 075 this.authenticatedUserID = authenticatedUserID; 076 this.resourceDescriptor = resourceDescriptor; 077 this.httpServletRequest = null; 078 this.ifMatchHeaderValue = null; 079 this.ifNoneMatchHeaderValue = null; 080 } 081 082 083 084 /** 085 * Create a new SCIM request from the provided information. 086 * 087 * @param baseURL The base URL for the SCIM service. 088 * @param authenticatedUserID The authenticated user name or {@code null} if 089 * the request is not authenticated. 090 * @param resourceDescriptor The ResourceDescriptor associated with this 091 * request. 092 * @param httpServletRequest The HTTP servlet request associated with this 093 * request or {@code null} if this request is not 094 * initiated by a servlet. 095 */ 096 public SCIMRequest(final URI baseURL, final String authenticatedUserID, 097 final ResourceDescriptor resourceDescriptor, 098 final HttpServletRequest httpServletRequest) 099 { 100 this.baseURL = baseURL; 101 this.authenticatedUserID = authenticatedUserID; 102 this.resourceDescriptor = resourceDescriptor; 103 this.httpServletRequest = httpServletRequest; 104 this.ifMatchHeaderValue = 105 httpServletRequest.getHeader(HttpHeaders.IF_MATCH); 106 this.ifNoneMatchHeaderValue = 107 httpServletRequest.getHeader(HttpHeaders.IF_NONE_MATCH); 108 } 109 110 111 112 113 114 /** 115 * Create a new SCIM request from the provided information. 116 * 117 * @param baseURL The base URL for the SCIM service. 118 * @param authenticatedUserID The authenticated user name or {@code null} if 119 * the request is not authenticated. 120 * @param resourceDescriptor The ResourceDescriptor associated with this 121 * request. 122 * @param httpServletRequest The HTTP servlet request associated with this 123 * request or {@code null} if this request is not 124 * initiated by a servlet. 125 * @param ifMatchHeaderValue The If-Match header value. 126 * @param ifNoneMatchHeaderValue The If-None-Match header value. 127 */ 128 public SCIMRequest(final URI baseURL, final String authenticatedUserID, 129 final ResourceDescriptor resourceDescriptor, 130 final HttpServletRequest httpServletRequest, 131 final String ifMatchHeaderValue, 132 final String ifNoneMatchHeaderValue) 133 { 134 this.baseURL = baseURL; 135 this.authenticatedUserID = authenticatedUserID; 136 this.resourceDescriptor = resourceDescriptor; 137 this.httpServletRequest = httpServletRequest; 138 this.ifMatchHeaderValue = ifMatchHeaderValue; 139 this.ifNoneMatchHeaderValue = ifNoneMatchHeaderValue; 140 } 141 142 143 144 /** 145 * Retrieve the base URL for the SCIM service. 146 * 147 * @return The base URL for the SCIM service. 148 */ 149 public URI getBaseURL() 150 { 151 return baseURL; 152 } 153 154 155 156 /** 157 * Get the authenticated user ID. 158 * 159 * @return The authenticated user ID or {@code null} if the request is 160 * not authenticated. 161 */ 162 public String getAuthenticatedUserID() 163 { 164 return authenticatedUserID; 165 } 166 167 168 169 /** 170 * Get ResourceDescriptor associated with this request. 171 * 172 * @return The ResourceDescriptor associated with this request. 173 */ 174 public ResourceDescriptor getResourceDescriptor() { 175 return resourceDescriptor; 176 } 177 178 179 180 /** 181 * Get the HTTP servlet request associated with this request. 182 * 183 * @return The HTTP servlet request associated with this request or 184 * {@code null} if this request is not initiated by a servlet. 185 */ 186 public HttpServletRequest getHttpServletRequest() { 187 return httpServletRequest; 188 } 189 190 191 192 /** 193 * Evaluate request preconditions for a resource that does not currently 194 * exist. The primary use of this method is to support the <a 195 * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24"> 196 * If-Match: *</a> and <a 197 * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26"> 198 * If-None-Match: *</a> preconditions. 199 * 200 * @param exception The ResourceNotFoundException that would've been thrown 201 * if the preconditions are met. 202 * @throws SCIMException if preconditions have not been met. 203 */ 204 public void checkPreconditions(final ResourceNotFoundException exception) 205 throws SCIMException 206 { 207 // According to RFC 2616 14.24, If-Match: 208 // if "*" is given and no current entity exists, the server MUST NOT 209 // perform the requested method, and MUST return a 412 (Precondition Failed) 210 // response. 211 if (ifMatchHeaderValue != null && 212 parseMatchHeader(ifMatchHeaderValue).isEmpty()) 213 { 214 throw new PreconditionFailedException(exception.getMessage()); 215 } 216 } 217 218 219 220 /** 221 * Evaluate request preconditions based on the passed in current version. 222 * 223 * @param currentVersion an ETag for the current version of the resource 224 * 225 * @throws SCIMException if preconditions have not been met. 226 */ 227 public void checkPreconditions(final EntityTag currentVersion) 228 throws SCIMException 229 { 230 if (ifMatchHeaderValue != null) 231 { 232 evaluateIfMatch(currentVersion, ifMatchHeaderValue); 233 } 234 else if (ifNoneMatchHeaderValue != null) 235 { 236 evaluateIfNoneMatch(currentVersion, ifNoneMatchHeaderValue); 237 } 238 } 239 240 /** 241 * Evaluate If-Match header against the provided eTag. 242 * 243 * @param eTag The current eTag. 244 * @param headerValue The If-Match header value. 245 * @throws SCIMException If a match was not found or parsing error occurs. 246 */ 247 protected void evaluateIfMatch(final EntityTag eTag, final String headerValue) 248 throws SCIMException 249 { 250 List<EntityTag> eTags = parseMatchHeader(headerValue); 251 252 if (!isMatch(eTags, eTag)) 253 { 254 255 throw new PreconditionFailedException( 256 "Resource changed since last retrieved", eTag.toString(), null); 257 } 258 } 259 260 /** 261 * Evaluate If-None-Match header against the provided eTag. 262 * 263 * @param eTag The current eTag. 264 * @param headerValue The If-None-Match header value. 265 * @throws SCIMException If a match was found or parsing error occurs. 266 */ 267 protected void evaluateIfNoneMatch(final EntityTag eTag, 268 final String headerValue) 269 throws SCIMException 270 { 271 List<EntityTag> eTags = parseMatchHeader(headerValue); 272 273 if (isMatch(eTags, eTag)) 274 { 275 throw new PreconditionFailedException( 276 "Resource did not change since last retrieved", 277 eTag.toString(), null); 278 } 279 } 280 281 /** 282 * Evaluate if the provided eTag matches any of the eTags in the provided 283 * list. 284 * 285 * @param eTags The list of eTags to find matches in. 286 * @param eTag The eTag to match. 287 * @return {@code true} if a match was found or {@code false} otherwise. 288 */ 289 private boolean isMatch(final List<EntityTag> eTags, final EntityTag eTag) 290 { 291 if (eTag == null) { 292 return false; 293 } 294 if (eTags.isEmpty()) { 295 return true; 296 } 297 String value = eTag.getValue(); 298 for (EntityTag e : eTags) { 299 if (value.equals(e.getValue())) { 300 return true; 301 } 302 } 303 return false; 304 } 305 306 /** 307 * Parse the value of an If-Match or If-None-Match header value. 308 * 309 * @param headerValue The header value to parse. 310 * @return The parsed eTags or an empty list if a wildcard eTag was parsed. 311 * @throws InvalidResourceException If an error occurred during parsing. 312 */ 313 private List<EntityTag> parseMatchHeader(final String headerValue) 314 throws InvalidResourceException 315 { 316 List<EntityTag> versions = null; 317 318 if(headerValue != null) 319 { 320 String[] valueTokens = headerValue.split(","); 321 versions = new ArrayList<EntityTag>(valueTokens.length); 322 for(String token : valueTokens) 323 { 324 token = token.trim(); 325 326 if(token.equals("*")) 327 { 328 return Collections.emptyList(); 329 } 330 331 EntityTag tag; 332 try 333 { 334 tag = EntityTag.valueOf(token); 335 } 336 catch(IllegalArgumentException e) 337 { 338 throw new InvalidResourceException(e.getMessage(), e); 339 } 340 versions.add(tag); 341 } 342 } 343 344 return versions; 345 } 346}