You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@olingo.apache.org by mi...@apache.org on 2013/09/15 16:04:33 UTC

git commit: Moved content type validation from RestUtil to ContentType

Updated Branches:
  refs/heads/master 9ee8e0f3a -> 95a43b421


Moved content type validation from RestUtil to ContentType


Project: http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/commit/95a43b42
Tree: http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/tree/95a43b42
Diff: http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/diff/95a43b42

Branch: refs/heads/master
Commit: 95a43b421305dae448284e891971d61c76255df6
Parents: 9ee8e0f
Author: Michael Bolz <mi...@apache.org>
Authored: Sun Sep 15 15:56:07 2013 +0200
Committer: Michael Bolz <mi...@apache.org>
Committed: Sun Sep 15 15:58:04 2013 +0200

----------------------------------------------------------------------
 .../olingo/odata2/core/ContentNegotiator.java   |  2 +
 .../olingo/odata2/core/commons/ContentType.java | 97 ++++++++++++++++++--
 .../olingo/odata2/core/rest/RestUtil.java       | 19 ++--
 .../odata2/core/commons/ContentTypeTest.java    | 21 +++++
 .../odata2/fit/basic/AcceptHeaderTypeTest.java  | 10 +-
 .../olingo/odata2/fit/basic/FitLoadTest.java    |  8 +-
 .../BasicContentNegotiationTest.java            | 13 ++-
 7 files changed, 142 insertions(+), 28 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/95a43b42/odata-core/src/main/java/org/apache/olingo/odata2/core/ContentNegotiator.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/ContentNegotiator.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/ContentNegotiator.java
index 309a79f..8994d57 100644
--- a/odata-core/src/main/java/org/apache/olingo/odata2/core/ContentNegotiator.java
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/ContentNegotiator.java
@@ -19,6 +19,7 @@
 package org.apache.olingo.odata2.core;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -63,6 +64,7 @@ public class ContentNegotiator {
       if(uriInfo.getUriType() == UriType.URI5 || uriInfo.getUriType() == UriType.URI4) {
         return ContentType.TEXT_PLAIN_CS_UTF_8;        
       }
+      return doContentNegotiationForAcceptHeader(Arrays.asList("*/*"), ContentType.create(supportedContentTypes));
     } 
     
     if (uriInfo.getFormat() == null) {

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/95a43b42/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java
index 037f70c..5a174e2 100644
--- a/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/commons/ContentType.java
@@ -31,6 +31,7 @@ import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.TreeMap;
+import java.util.regex.Pattern;
 
 /**
  * Internally used {@link ContentType} for OData library.
@@ -86,9 +87,20 @@ public class ContentType {
     KNOWN_MIME_TYPES.add("multipart");
     KNOWN_MIME_TYPES.add("text");
   }
+  
+  private static final Comparator<String> Q_PARAMETER_COMPARATOR = new Comparator<String>() {
+    @Override
+    public int compare(String o1, String o2) {
+      Float f1 = parseQParameterValue(o1);
+      Float f2 = parseQParameterValue(o2);
+      return f2.compareTo(f1);
+    }
+  };
+
 
   private static final char WHITESPACE_CHAR = ' ';
   private static final String PARAMETER_SEPARATOR = ";";
+  private static final String PARAMETER_KEY_VALUE_SEPARATOR = "=";
   private static final String TYPE_SUBTYPE_SEPARATOR = "/";
   private static final String MEDIA_TYPE_WILDCARD = "*";
   private static final String VERBOSE = "verbose";
@@ -99,6 +111,8 @@ public class ContentType {
   public static final String PARAMETER_TYPE = "type";
   public static final String CHARSET_UTF_8 = "utf-8";
 
+  private static final Pattern Q_PARAMETER_VALUE_PATTERN = Pattern.compile("1|0|1\\.0{1,3}|0\\.\\d{1,3}");
+
   public static final ContentType WILDCARD = new ContentType(MEDIA_TYPE_WILDCARD, MEDIA_TYPE_WILDCARD);
 
   public static final ContentType APPLICATION_XML = new ContentType("application", "xml", ODataFormat.XML);
@@ -192,6 +206,20 @@ public class ContentType {
   }
 
   /**
+   * Validates if given <code>format</code> is parseable and can be used as input for
+   * {@link #create(String)} method.
+   * @param format to be validated string
+   * @return <code>true</code> if format is parseable otherwise <code>false</code>
+   */
+  public static boolean isParseableAsCustom(final String format) {
+    try {
+      return ContentType.create(format) != null;
+    } catch (IllegalArgumentException e) {
+      return false;
+    }
+  }
+
+  /**
    * Creates a content type from type and subtype
    * @param type
    * @param subtype
@@ -364,6 +392,19 @@ public class ContentType {
   }
 
   /**
+   * Sort given list (which must contains content type formated string) for their {@value #PARAMETER_Q} value
+   * as defined in <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1">RFC 2616 section 4.1</a> and 
+   * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.9">RFC 2616 Section 3.9</a>.
+   * 
+   * <b>Attention:</b> For invalid values a {@value #PARAMETER_Q} value from <code>-1</code> is used for sorting.
+   * 
+   * @param toSort list which is sorted and hence re-arranged
+   */
+  public static void sortForQParameter(List<String> toSort) {
+    Collections.sort(toSort, ContentType.Q_PARAMETER_COMPARATOR);
+  }
+  
+  /**
    * Map combination of type/subtype to corresponding {@link ODataFormat}.
    * 
    * @param type type which is used for mapping
@@ -406,6 +447,7 @@ public class ContentType {
   /**
    * Valid input are <code>;</code> separated <code>key=value</code> pairs 
    * without spaces between key and value.
+   * <b>Attention:</b> <code>q</code> parameter is validated but not added to result map
    * 
    * <p>
    * See RFC 2616:
@@ -423,13 +465,18 @@ public class ContentType {
     if (parameters != null) {
       String[] splittedParameters = parameters.split(PARAMETER_SEPARATOR);
       for (String parameter : splittedParameters) {
-        String[] keyValue = parameter.split("=");
+        String[] keyValue = parameter.split(PARAMETER_KEY_VALUE_SEPARATOR);
         String key = keyValue[0].trim().toLowerCase(Locale.ENGLISH);
-        if (isParameterAllowed(key)) {
-          String value = keyValue.length > 1 ? keyValue[1] : null;
-          if (value != null && isLws(value.charAt(0))) {
-            throw new IllegalArgumentException("Value of parameter '" + key + "' starts with a LWS ('" + parameters + "').");
+        String value = keyValue.length > 1 ? keyValue[1] : null;
+        if (value != null && isLws(value.charAt(0))) {
+          throw new IllegalArgumentException("Value of parameter '" + key + "' starts with a LWS ('" + parameters + "').");
+        }
+        if(PARAMETER_Q.equals(key.toLowerCase(Locale.US))) {
+          // q parameter is only validated but not added
+          if(!Q_PARAMETER_VALUE_PATTERN.matcher(value).matches()) {
+            throw new IllegalArgumentException("Value of 'q' parameter is not valid (q='" + value + "').");            
           }
+        } else {
           parameterMap.put(key, value);
         }
       }
@@ -437,6 +484,43 @@ public class ContentType {
     return parameterMap;
   }
 
+  /**
+   * Parse value of {@value #PARAMETER_Q} <code>parameter</code> out of content type/parameters.
+   * If no {@value #PARAMETER_Q} <code>parameter</code> is in <code>content type/parameters</code> parameter found
+   * <code>1</code> is returned.
+   * If {@value #PARAMETER_Q} <code>parameter</code> is invalid <code>-1</code> is returned.
+   * 
+   * @param contentType parameter which is parsed for {@value #PARAMETER_Q} <code>parameter</code> value
+   * @return value of {@value #PARAMETER_Q} <code>parameter</code> or <code>1</code> or <code>-1</code>
+   */
+  private static Float parseQParameterValue(final String contentType) {
+    if (contentType != null) {
+      String[] splittedParameters = contentType.split(PARAMETER_SEPARATOR);
+      for (String parameter : splittedParameters) {
+        String[] keyValue = parameter.split(PARAMETER_KEY_VALUE_SEPARATOR);
+        String key = keyValue[0].trim().toLowerCase(Locale.ENGLISH);
+        if(PARAMETER_Q.equalsIgnoreCase(key)) {
+          String value = keyValue.length > 1 ? keyValue[1] : null;
+          if (Q_PARAMETER_VALUE_PATTERN.matcher(value).matches()) {
+            return Float.valueOf(value);
+          }
+          return Float.valueOf(-1);
+        }
+      }
+    }
+    return Float.valueOf(1);
+  }
+
+  /**
+   * Check if parameter with key value is a allowed parameter.
+   * 
+   * @param key
+   * @return
+   */
+  private static boolean isParameterAllowed(final String key) {
+    return key != null && !PARAMETER_Q.equals(key.toLowerCase(Locale.US));
+  }
+
   /** 
    * Validate if given character is a linear whitepace (includes <code>horizontal-tab, linefeed, carriage return and space</code>).
    * 
@@ -455,9 +539,6 @@ public class ContentType {
     }
   }
 
-  private static boolean isParameterAllowed(final String key) {
-    return key != null && !PARAMETER_Q.equals(key.toLowerCase(Locale.US));
-  }
 
   /**
    * Ensure that charset parameter ({@link #PARAMETER_CHARSET}) is set on returned content type

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/95a43b42/odata-core/src/main/java/org/apache/olingo/odata2/core/rest/RestUtil.java
----------------------------------------------------------------------
diff --git a/odata-core/src/main/java/org/apache/olingo/odata2/core/rest/RestUtil.java b/odata-core/src/main/java/org/apache/olingo/odata2/core/rest/RestUtil.java
index 56b883d..aa46924 100644
--- a/odata-core/src/main/java/org/apache/olingo/odata2/core/rest/RestUtil.java
+++ b/odata-core/src/main/java/org/apache/olingo/odata2/core/rest/RestUtil.java
@@ -27,12 +27,12 @@ import java.net.URISyntaxException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 
 import javax.servlet.ServletInputStream;
 import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.ResponseBuilder;
@@ -128,29 +128,22 @@ public class RestUtil {
   }
 
   public static List<String> extractAcceptHeaders(final SubLocatorParameter param) throws ODataBadRequestException {
-    // first validate all accept header content types are 'parseable' and valif from our point of view
     List<String> acceptHeaders = param.getHttpHeaders().getRequestHeader(HttpHeaders.ACCEPT);
 
+    List<String> toSort = new LinkedList<String>();
     if (acceptHeaders != null) {
       for (String acceptHeader : acceptHeaders) {
         String[] contentTypes = acceptHeader.split(",");
         for (String contentType : contentTypes) {
-          if (!ContentType.isParseable(contentType.trim())) {
-            throw new ODataBadRequestException(ODataBadRequestException.INVALID_HEADER.addContent(HttpHeaders.ACCEPT, acceptHeader));
-          }
+          toSort.add(contentType.trim());
         }
       }
     }
 
-    // then get sorted list of acceptable media type from jax-rs
-    final List<String> mediaTypes = new ArrayList<String>();
-    final List<MediaType> acceptableMediaTypes = param.getHttpHeaders().getAcceptableMediaTypes();
-    for (final MediaType mediaType : acceptableMediaTypes) {
-      mediaTypes.add(mediaType.toString());
-    }
-
-    return mediaTypes;
+    ContentType.sortForQParameter(toSort);
+    return toSort;
   }
+  
 
   public static Map<String, String> extractRequestHeaders(final javax.ws.rs.core.HttpHeaders httpHeaders) {
     final MultivaluedMap<String, String> headers = httpHeaders.getRequestHeaders();

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/95a43b42/odata-core/src/test/java/org/apache/olingo/odata2/core/commons/ContentTypeTest.java
----------------------------------------------------------------------
diff --git a/odata-core/src/test/java/org/apache/olingo/odata2/core/commons/ContentTypeTest.java b/odata-core/src/test/java/org/apache/olingo/odata2/core/commons/ContentTypeTest.java
index 7734df4..14cac0f 100644
--- a/odata-core/src/test/java/org/apache/olingo/odata2/core/commons/ContentTypeTest.java
+++ b/odata-core/src/test/java/org/apache/olingo/odata2/core/commons/ContentTypeTest.java
@@ -971,6 +971,27 @@ public class ContentTypeTest extends BaseTest {
     assertTrue(ContentType.create("*/*").hasWildcard());
   }
 
+  @Test
+  public void testQParameterSort() {
+    validateSort(Arrays.asList("a1/b1;q=0.2", "a2/b2;q=0.5", "a3/b3;q=0.333"), 1,2,0);
+    validateSort(Arrays.asList("a1/b1;q=0", "a2/b2;q=0.5", "a3/b3;q=0.333"), 1,2,0);
+    validateSort(Arrays.asList("a1/b1;q=1", "a2/b2;q=0.5", "a3/b3;q=0.333"), 0,1,2);
+    validateSort(Arrays.asList("a1/b1;q=1", "a2/b2;q=0.5", "a3/b3;q=1.333"), 0,1,2);
+    validateSort(Arrays.asList("a1/b1;q=0.2", "a2/b2;q=0.9", "a3/b3"), 2,1,0);
+  }
+
+  private void validateSort(List<String> toSort, int ... expectedSequence) {
+    List<String> expected = new ArrayList<String>();
+    for (int i : expectedSequence) {
+      expected.add(toSort.get(i));
+    }
+
+    ContentType.sortForQParameter(toSort);
+    for (int i = 0; i < expectedSequence.length; i++) {
+      assertEquals(expected.get(i), toSort.get(i));
+    }
+  }
+  
   private Map<String, String> addParameters(final String... content) {
     Map<String, String> map = new HashMap<String, String>();
     for (int i = 0; i < content.length - 1; i += 2) {

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/95a43b42/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/AcceptHeaderTypeTest.java
----------------------------------------------------------------------
diff --git a/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/AcceptHeaderTypeTest.java b/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/AcceptHeaderTypeTest.java
index eb9b56d..59b2397 100644
--- a/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/AcceptHeaderTypeTest.java
+++ b/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/AcceptHeaderTypeTest.java
@@ -31,8 +31,6 @@ import org.apache.http.HttpHeaders;
 import org.apache.http.HttpResponse;
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.methods.HttpGet;
-import org.junit.Test;
-
 import org.apache.olingo.odata2.api.commons.HttpStatusCodes;
 import org.apache.olingo.odata2.api.edm.provider.EdmProvider;
 import org.apache.olingo.odata2.api.exception.ODataException;
@@ -42,6 +40,7 @@ import org.apache.olingo.odata2.api.uri.info.GetEntitySetUriInfo;
 import org.apache.olingo.odata2.api.uri.info.GetEntityUriInfo;
 import org.apache.olingo.odata2.api.uri.info.GetMetadataUriInfo;
 import org.apache.olingo.odata2.testutil.mock.EdmTestProvider;
+import org.junit.Test;
 
 /**
  *  
@@ -128,8 +127,13 @@ public class AcceptHeaderTypeTest extends AbstractBasicTest {
   }
 
   @Test
+  public void illegalQParameterInContentType() throws Exception {
+    testGetRequest("Rooms('1')", "application/json;q=2", HttpStatusCodes.BAD_REQUEST, "application/xml");
+  }
+
+  @Test
   public void illegalSpaceInContentType() throws Exception {
-    testGetRequest("Rooms('1')", "application    /json", HttpStatusCodes.BAD_REQUEST, "application/json");
+    testGetRequest("Rooms('1')", "application    /json", HttpStatusCodes.BAD_REQUEST, "application/xml");
   }
 
   @Test

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/95a43b42/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/FitLoadTest.java
----------------------------------------------------------------------
diff --git a/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/FitLoadTest.java b/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/FitLoadTest.java
index 3462fd3..2cc063a 100644
--- a/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/FitLoadTest.java
+++ b/odata-fit/src/test/java/org/apache/olingo/odata2/fit/basic/FitLoadTest.java
@@ -27,11 +27,12 @@ import java.io.IOException;
 import java.net.HttpURLConnection;
 import java.net.URI;
 
+import javax.ws.rs.core.HttpHeaders;
+
 import org.apache.http.HttpResponse;
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.methods.HttpGet;
-import org.junit.Test;
-
+import org.apache.olingo.odata2.api.commons.HttpContentType;
 import org.apache.olingo.odata2.api.commons.HttpStatusCodes;
 import org.apache.olingo.odata2.api.exception.ODataException;
 import org.apache.olingo.odata2.api.processor.ODataResponse;
@@ -40,6 +41,7 @@ import org.apache.olingo.odata2.api.processor.part.MetadataProcessor;
 import org.apache.olingo.odata2.api.processor.part.ServiceDocumentProcessor;
 import org.apache.olingo.odata2.api.uri.info.GetMetadataUriInfo;
 import org.apache.olingo.odata2.api.uri.info.GetServiceDocumentUriInfo;
+import org.junit.Test;
 
 /**
  *  
@@ -76,8 +78,10 @@ public class FitLoadTest extends AbstractBasicTest {
     for (int i = 0; i < LOOP_COUNT; i++) {
       HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
       connection.setRequestMethod("GET");
+      connection.setRequestProperty(HttpHeaders.ACCEPT, HttpContentType.WILDCARD);
       connection.connect();
       assertEquals(HttpStatusCodes.OK.getStatusCode(), connection.getResponseCode());
+      assertEquals(HttpContentType.APPLICATION_XML_UTF8, connection.getContentType());
     }
   }
 

http://git-wip-us.apache.org/repos/asf/incubator-olingo-odata2/blob/95a43b42/odata-fit/src/test/java/org/apache/olingo/odata2/fit/ref/contentnegotiation/BasicContentNegotiationTest.java
----------------------------------------------------------------------
diff --git a/odata-fit/src/test/java/org/apache/olingo/odata2/fit/ref/contentnegotiation/BasicContentNegotiationTest.java b/odata-fit/src/test/java/org/apache/olingo/odata2/fit/ref/contentnegotiation/BasicContentNegotiationTest.java
index 4ce1eea..460317d 100644
--- a/odata-fit/src/test/java/org/apache/olingo/odata2/fit/ref/contentnegotiation/BasicContentNegotiationTest.java
+++ b/odata-fit/src/test/java/org/apache/olingo/odata2/fit/ref/contentnegotiation/BasicContentNegotiationTest.java
@@ -72,8 +72,6 @@ public class BasicContentNegotiationTest extends AbstractContentNegotiationTest
     final String parameter = ";odata=verbose";
     performRequestAndValidateResponseForAcceptHeader("Employees('1')", APPLICATION_JSON + parameter, APPLICATION_JSON + parameter);
     performRequestAndValidateResponseForAcceptHeader("Employees('1')", APPLICATION_JSON_UTF8 + parameter, APPLICATION_JSON_UTF8 + parameter);
-//    performRequestAndValidateResponseForAcceptHeader("Rooms('1')", APPLICATION_XML + parameter, APPLICATION_XML_UTF8 + parameter);
-//    performRequestAndValidateResponseForAcceptHeader("Employees('1')", APPLICATION_XML_UTF8 + parameter, APPLICATION_XML_UTF8 + parameter);
     performRequestAndValidateResponseForAcceptHeader("Employees('1')/$count", APPLICATION_XML_UTF8 + parameter, TEXT_PLAIN_UTF8);
     performRequestAndValidateResponseForAcceptHeader("Employees('1')/$count", APPLICATION_JSON + parameter, TEXT_PLAIN_UTF8 );
     performRequestAndValidateResponseForAcceptHeader("Buildings('1')/$count", APPLICATION_XML_UTF8 + parameter, TEXT_PLAIN_UTF8);
@@ -114,6 +112,17 @@ public class BasicContentNegotiationTest extends AbstractContentNegotiationTest
   }
 
   @Test
+  public void acceptHeaderToContentTypeIgnoredAcceptHeadersValue() throws Exception {
+    final String expectedImageContentType = "image/jpeg";
+    performRequestAndValidateResponseForAcceptHeader("Employees('1')/$value", APPLICATION_XML_UTF8, expectedImageContentType);
+    performRequestAndValidateResponseForAcceptHeader("Employees('1')/$value", APPLICATION_JSON, expectedImageContentType);
+
+    final String expectedTextContentType = "text/plain;charset=utf-8";
+    performRequestAndValidateResponseForAcceptHeader("Employees('1')/Age/$value", APPLICATION_XML_UTF8, expectedTextContentType);
+    performRequestAndValidateResponseForAcceptHeader("Employees('1')/Age/$value", APPLICATION_JSON, expectedTextContentType);
+  }
+  
+  @Test
   public void acceptHeaderToContentTypeNotAcceptable() throws Exception {
     final String parameterUnknown = ";someUnknownParameter=withAValue";
     performRequestAndValidateResponseForNotAcceptable("Employees('1')", APPLICATION_JSON + parameterUnknown);