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 &lt;a
195   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24"&gt;
196   * If-Match: *&lt;/a&gt; and &lt;a
197   * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26"&gt;
198   * If-None-Match: *&lt;/a&gt; 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}