cxf-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From bimargul...@apache.org
Subject svn commit: r1209463 - in /cxf/trunk: rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/ systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/ systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/
Date Fri, 02 Dec 2011 13:25:10 GMT
Author: bimargulies
Date: Fri Dec  2 13:25:10 2011
New Revision: 1209463

URL: http://svn.apache.org/viewvc?rev=1209463&view=rev
Log:
CXF-3493: fix up much confusion, more tests pass.

Added:
    cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java
  (with props)
    cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java
  (contents, props changed)
      - copied, changed from r1209425, cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java
Removed:
    cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java
Modified:
    cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java
    cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java
    cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java
    cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml

Modified: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java
URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java?rev=1209463&r1=1209462&r2=1209463&view=diff
==============================================================================
--- cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java
(original)
+++ cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharing.java
Fri Dec  2 13:25:10 2011
@@ -53,17 +53,17 @@ public @interface CrossOriginResourceSha
      * A list of permitted origins. This is ignored 
      * if {@link #allowAllOrigins()} is true.
      */
-    String[] allowOrigins();
+    String[] allowOrigins() default { };
     /**
      * A list of HTTP methods. This is used only for preflight,
      * and is only valid on a class.
      */
-    String[] allowMethods();
+    String[] allowMethods() default { };
     /**
      * A list of headers that the client may include
      * in an actual request.
      */
-    String[] allowHeaders();
+    String[] allowHeaders() default { };
     /**
      * If true, this resource will return 
      * <pre>Access-Control-Allow-Credentials: true</pre>
@@ -73,7 +73,7 @@ public @interface CrossOriginResourceSha
      * A list of headers to return in <tt>
      * Access-Control-Expose-Headers</tt>. 
      */
-    String[] exposeHeaders();
+    String[] exposeHeaders() default { };
     /**
      * The value to return in <tt>Access-Control-Max-Age</tt>.
      * If this is negative, then no header is returned. The default
@@ -89,4 +89,10 @@ public @interface CrossOriginResourceSha
      * performs preflight processing.
      */
     boolean localPreflight() default false;
+    
+    /**
+     * For use inside @{@link CrossOriginResourceSharingPaths}. The path to apply the
+     * policies to.
+     */
+    String path() default "";
 }

Modified: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java
URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java?rev=1209463&r1=1209462&r2=1209463&view=diff
==============================================================================
--- cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java
(original)
+++ cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingFilter.java
Fri Dec  2 13:25:10 2011
@@ -19,9 +19,11 @@
 
 package org.apache.cxf.jaxrs.cors;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.regex.Pattern;
 
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
@@ -33,6 +35,7 @@ import org.apache.cxf.jaxrs.ext.RequestH
 import org.apache.cxf.jaxrs.ext.ResponseHandler;
 import org.apache.cxf.jaxrs.model.ClassResourceInfo;
 import org.apache.cxf.jaxrs.model.OperationResourceInfo;
+import org.apache.cxf.jaxrs.utils.HttpUtils;
 import org.apache.cxf.message.Message;
 
 /**
@@ -41,9 +44,19 @@ import org.apache.cxf.message.Message;
  * information in the Exchange to allow the response handler to add the appropriate headers
to the response.
  * If you need complex or subtle control of the behavior here (e.g. clearing the prefight
cache) you might be
  * better off reading the source of this and implementing this inside your service.
+ * 
+ * This class will perform preflight processing even if there is a resource method annotated

+ * to handle @OPTIONS,
+ * <em>unless</em> that method is annotated as follows:
+ * <pre>
+ *   @CrossOriginResourceSharing(localPreflight = true)
+ * </pre>
+ * or unless the <tt>defaultOptionsMethodsHandlePreflight</tt> property of this
class is set to <tt>true</tt>.
  */
 public class CrossOriginResourceSharingFilter implements RequestHandler, ResponseHandler
{
-
+    private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
+    private static final Pattern FIELD_COMMA_PATTERN = Pattern.compile(",\\w*");
+    
     @Context
     private HttpHeaders headers;
 
@@ -58,8 +71,12 @@ public class CrossOriginResourceSharingF
     private boolean allowCredentials;
     private List<String> exposeHeaders = Collections.emptyList();
     private Integer maxAge;
+    private boolean defaultOptionsMethodsHandlePreflight;
 
     private CrossOriginResourceSharing getAnnotation(OperationResourceInfo ori) {
+        if (ori == null) {
+            return null;
+        }
         return ReflectionUtil.getAnnotationForMethodOrContainingClass(ori.getAnnotatedMethod(),
                                                                       CrossOriginResourceSharing.class);
     }
@@ -69,37 +86,41 @@ public class CrossOriginResourceSharingF
         CrossOriginResourceSharing annotation = getAnnotation(opResInfo);
 
         if ("OPTIONS".equals(m.get(Message.HTTP_REQUEST_METHOD))) {
-            // what if someone wants to use options for something else, and also for preflight?
-            // in that case, they set the localPreflight flag, and we bow out.
-            if (opResInfo != null && (annotation == null || annotation.localPreflight()))
{
-                return null; // continue handling
-            }
-            return preflightRequest(m, annotation, resourceClass);
+          
+            return preflightRequest(m, annotation, opResInfo, resourceClass);
         }
         return simpleRequest(m, annotation);
     }
 
     private Response simpleRequest(Message m, CrossOriginResourceSharing ann) {
-        List<String> values = headers.getRequestHeader(CorsHeaderConstants.HEADER_ORIGIN);
+        List<String> values = getHeaderValues(CorsHeaderConstants.HEADER_ORIGIN, true);
         // 5.1.1 there has to be an origin
         if (values == null || values.size() == 0) {
             return null;
         }
+        
         // 5.1.2 check all the origins
         if (!effectiveAllowAllOrigins(ann) && !effectiveAllowOrigins(ann).containsAll(values))
{
             return null;
         }
+        
+        String originResponse;
         // 5.1.3 credentials lives in the output filter
         // in any case
         if (effectiveAllowAllOrigins(ann)) {
-            m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, Arrays.asList(new String[]
{
-                "*"
-            }));
+            originResponse = "*";
         } else {
-            m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, values);
+            originResponse = concatValues(values, true);
         }
 
-        // 5.1.4 expose headers lives on the output side.
+        // handle 5.1.3
+        commonRequestProcessing(m, ann, originResponse);
+        
+        // 5.1.4
+        List<String> effectiveExposeHeaders = effectiveExposeHeaders(ann);
+        if (effectiveExposeHeaders != null && effectiveExposeHeaders.size() != 0)
{
+            m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, effectiveExposeHeaders);
+        }
 
         // note what kind of processing we're doing.
         m.getExchange().put(CrossOriginResourceSharingFilter.class.getName(), "simple");
@@ -109,47 +130,84 @@ public class CrossOriginResourceSharingF
     /**
      * handle preflight.
      * 
+     * Note that preflight is a bit of a parasite on OPTIONS. The class may still have an
options method,
+     * and, if it does, it will be invoked, and it will respond however it likes. The response
will
+     * have additional headers based on what happens here.
+     * 
      * @param m the incoming message.
+     * @param opResInfo 
      * @param ann the annotation, if any, derived from a method that matched the OPTIONS
request for the
      *            preflight. probably completely useless.
      * @param resourceClass the resource class passed into the filter.
      * @return
      */
+    //CHECKSTYLE:OFF
     private Response preflightRequest(Message m, CrossOriginResourceSharing optionAnn,
-                                      ClassResourceInfo resourceClass) {
+                                      OperationResourceInfo opResInfo, ClassResourceInfo
resourceClass) {
+
         /*
-         * CORS doesn't send enough information with a preflight to accurately identity the
single method
-         * that will handle the request. So the code uses annotations from the containing
class,
-         * only. 
+         * What to do if the resource class indeed has a method annotated with @OPTIONS 
+         * that is matched by this request? We go ahead and do this job unless the request
+         * has one of our annotations on it (or its parent class) indicating 'localPreflight'
--
+         * or the defaultOptionsMethodsHandlePreflight flag is true.
          */
-        CrossOriginResourceSharing ann 
-            = resourceClass.getResourceClass().getAnnotation(CrossOriginResourceSharing.class);
+        if (opResInfo != null && ((optionAnn == null && defaultOptionsMethodsHandlePreflight)

+            || (optionAnn != null && optionAnn.localPreflight()))) {
+            return null; // let the resource method take all responsibility.
+        }
         
-        List<String> values = headers.getRequestHeader(CorsHeaderConstants.HEADER_ORIGIN);
+        List<String> headerOriginValues = getHeaderValues(CorsHeaderConstants.HEADER_ORIGIN,
true);
         String origin;
         // 5.2.1 -- must have origin, must have one origin.
-        if (values == null || values.size() != 1) {
-            return null;
-        }
-        origin = values.get(0);
-        // 5.2.2 must be on the list or we must be matching *.
-        boolean effectiveAllowAllOrigins = effectiveAllowAllOrigins(ann);
-        if (!effectiveAllowAllOrigins && !effectiveAllowOrigins(ann).contains(origin))
{
+        if (headerOriginValues == null || headerOriginValues.size() != 1) {
             return null;
         }
+        origin = headerOriginValues.get(0);
 
-        values = headers.getRequestHeader(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD);
+        List<String> requestMethodValues = getHeaderValues(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD,
false);
 
         // 5.2.3 must have access-control-request-method, must be single-valued
         // we should reject parse errors but we cannot.
-        if (values == null || values.size() != 1) {
+        if (requestMethodValues == null || requestMethodValues.size() != 1) {
             return null;
         }
+        String requestMethod = requestMethodValues.get(0);
 
-        String requestMethod = values.get(0);
+        /*
+         * CORS doesn't send enough information with a preflight to accurately identity the
single method
+         * that will handle the request. CrossOriginResourceSharingPaths provides annotations
by path/method
+         * for this case. If none of those apply, a plain class level CrossOrginResourceSharing
is the
+         * best we can do.
+         */
+        String requestUri = HttpUtils.getPathToMatch(m, true);
+        CrossOriginResourceSharing ann = null;
+        CrossOriginResourceSharingPaths classPathsAnn = 
+            resourceClass.getResourceClass().getAnnotation(CrossOriginResourceSharingPaths.class);
+        if (classPathsAnn != null) {
+            /* search the path/method pair. */
+            for (CrossOriginResourceSharing pathAnn : classPathsAnn.value()) {
+                /* A very simple path policy! If someone wants to turn this into
+                 * searching up the tree, they are welcome.
+                 */
+                if (pathAnn.path() != null && pathAnn.path().equals(requestUri) 
+                    && Arrays.asList(pathAnn.allowMethods()).contains(requestMethod))
{
+                    ann = pathAnn;
+                    break;
+                }
+            }
+        }
+        if (ann == null) {
+            ann = resourceClass.getResourceClass().getAnnotation(CrossOriginResourceSharing.class);
+        }
+
+        // 5.2.2 must be on the list or we must be matching *.
+        boolean effectiveAllowAllOrigins = effectiveAllowAllOrigins(ann);
+        if (!effectiveAllowAllOrigins && !effectiveAllowOrigins(ann).contains(origin))
{
+            return null;
+        }
 
         // 5.2.4 get list of request headers. we should reject parse errors but we cannot.
-        List<String> requestHeaders = headers.getRequestHeader(CorsHeaderConstants.HEADER_AC_REQUEST_HEADERS);
+        List<String> requestHeaders = getHeaderValues(CorsHeaderConstants.HEADER_AC_REQUEST_HEADERS,
false);
 
         // 5.2.5 reject if the method is not on the list.
         List<String> effectiveAllowMethods = effectiveAllowMethods(ann);
@@ -164,77 +222,82 @@ public class CrossOriginResourceSharingF
         }
 
         // 5.2.7: add allow credentials and allow-origin as required: this lives in the Output
filter
+        String originResponse;
         if (effectiveAllowAllOrigins(ann)) {
-            m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, Arrays.asList(new String[]
{
-                "*"
-            }));
+            originResponse = "*";
         } else {
-            m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, origin);
+            originResponse = origin;
         }
-        // 5.2.8 max-age lives in the output filter.
         // 5.2.9 add allow-methods; we pass them from here to the output filter which actually
adds them.
-        m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_METHODS, Arrays.asList(new
String[] {
-            requestMethod
-        }));
+        m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_METHODS, Arrays.asList(requestMethod));
+        
         // 5.2.10 add allow-headers; we pass them from here to the output filter which actually
adds them.
         m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, requestHeaders);
+        
+        // 5.2.8 max-age lives in the output filter.
+        if (effectiveMaxAge(ann) != null) {
+            m.getExchange().put(CorsHeaderConstants.HEADER_AC_MAX_AGE,effectiveMaxAge(ann).toString());
+        }
+
+        // 5.2.7 is in here.
+        commonRequestProcessing(m, ann, originResponse);
+
         m.getExchange().put(CrossOriginResourceSharingFilter.class.getName(), "preflight");
         // and allow things to proceed to the output filter.
         return Response.ok().build();
     }
+    //CHECKSTYLE:ON
+
+    private void commonRequestProcessing(Message m, CrossOriginResourceSharing ann, String
origin) {
+        
+        m.getExchange().put(CorsHeaderConstants.HEADER_ORIGIN, origin);
+        
+        m.getExchange().put(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS, effectiveAllowCredentials(ann));
+
+
+    }
 
     public Response handleResponse(Message m, OperationResourceInfo ori, Response response)
{
         String op = (String)m.getExchange().get(CrossOriginResourceSharingFilter.class.getName());
         if (op == null) {
             return response; // we're not here.
         }
-        CrossOriginResourceSharing annotation;
 
-        List<String> originHeader = getHeadersFromInput(m, CorsHeaderConstants.HEADER_ORIGIN);
         ResponseBuilder rbuilder = Response.fromResponse(response);
+        
+        /* Common to simple and preflight */
+        rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_ORIGIN, 
+                        (String)m.getExchange().get(CorsHeaderConstants.HEADER_ORIGIN));
+        rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS,
+                        Boolean.toString(allowCredentials));
+        
         if ("simple".equals(op)) {
-            annotation = getAnnotation(ori);
-            // 5.1.3: add Allow-Origin supplied from the input side, plus allow-credentials
as requested
-            addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_ORIGIN, originHeader);
-            rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS,
-                            Boolean.toString(effectiveAllowCredentials(annotation)));
-            // 5.1.4 add allowed headers
-            List<String> rqAllowedHeaders = getHeadersFromInput(m,
-                                                                CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS);
-            if (rqAllowedHeaders != null) {
-                addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_METHODS, rqAllowedHeaders);
-            }
-            
-            List<String> effectiveExposeHeaders = effectiveExposeHeaders(annotation);
-            if (effectiveExposeHeaders.size() > 0) {
-                addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS, effectiveExposeHeaders);
+            /* 5.1.4 expose headers */
+            List<String> effectiveExposeHeaders 
+                = getHeadersFromInput(m, CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS);
+            if (effectiveExposeHeaders != null) {
+                addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS, 
+                           effectiveExposeHeaders, false);
             }
             // if someone wants to clear the cache, we can't help them.
             return rbuilder.build();
         } else {
-            annotation = ori.getAnnotatedMethod().getDeclaringClass()
-                    .getAnnotation(CrossOriginResourceSharing.class);
-            // preflight
-            // 5.2.7 add Allow-Origin supplied from the input side, plus allow-credentials
as requested
-            addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_ORIGIN, originHeader);
-            rbuilder.header(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS,
-                            Boolean.toString(allowCredentials));
             // 5.2.8 max-age
-            if (effectiveMaxAge(annotation) != null) {
-                rbuilder.header(CorsHeaderConstants.HEADER_AC_MAX_AGE, 
-                                effectiveMaxAge(annotation).toString());
+            String maValue = (String)m.getExchange().get(CorsHeaderConstants.HEADER_AC_MAX_AGE);
+            if (maValue != null) {
+                rbuilder.header(CorsHeaderConstants.HEADER_AC_MAX_AGE, maValue);
             }
             // 5.2.9 add allowed methods
             /*
              * Currently, input side just lists the one requested method, and spec endorses
that.
              */
             addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_METHODS,
-                       getHeadersFromInput(m, CorsHeaderConstants.HEADER_AC_ALLOW_METHODS));
+                       getHeadersFromInput(m, CorsHeaderConstants.HEADER_AC_ALLOW_METHODS),
false);
             // 5.2.10 add allowed headers
             List<String> rqAllowedHeaders = getHeadersFromInput(m,
                                                                 CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS);
             if (rqAllowedHeaders != null) {
-                addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, rqAllowedHeaders);
+                addHeaders(rbuilder, CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS, rqAllowedHeaders,
false);
             }
             return rbuilder.build();
 
@@ -313,6 +376,66 @@ public class CrossOriginResourceSharingF
             return maxAge;
         }
     }
+    
+    /**
+     * Function called to grab a list of strings left behind by the input side.
+     * @param m
+     * @param key
+     * @return
+     */
+    @SuppressWarnings("unchecked")
+    private List<String> getHeadersFromInput(Message m, String key) {
+        Object obj = m.getExchange().get(key);
+        if (obj instanceof List<?>) {
+            return (List<String>)obj;
+        }
+        return null;
+    }
+
+    /**
+     * CORS uses one header containing space-separated values (Origin) and then
+     * a raft of #field-name productions, which parse on commas and optional spaces.
+     * @param m
+     * @param key
+     * @return
+     */
+    private List<String> getHeaderValues(String key, boolean spaceSeparated) {
+        List<String> values = headers.getRequestHeader(key);
+        Pattern splitPattern;
+        if (spaceSeparated) {
+            splitPattern = SPACE_PATTERN;
+        } else {
+            splitPattern = FIELD_COMMA_PATTERN;
+        }
+        List<String> results = new ArrayList<String>();
+        for (String value : values) {
+            String[] items = splitPattern.split(value);
+            for (String item : items) {
+                results.add(item);
+            }
+        }
+        return results;
+    }
+    
+    private void addHeaders(ResponseBuilder rb, String key, List<String> values, boolean
spaceSeparated) {
+        String sb = concatValues(values, spaceSeparated);
+        rb.header(key, sb);
+    }
+
+    private String concatValues(List<String> values, boolean spaceSeparated) {
+        StringBuffer sb = new StringBuffer();
+        for (int x = 0; x < values.size(); x++) {
+            sb.append(values.get(x));
+            if (x != values.size() - 1) {
+                if (spaceSeparated) {
+                    sb.append(" ");
+                } else {
+                    sb.append(", ");
+                }
+            }
+        }
+        return sb.toString();
+    }
 
     /**
      * The origin strings to allow. Call {@link #setAllowAllOrigins(boolean)} to enable '*'.
@@ -330,8 +453,9 @@ public class CrossOriginResourceSharingF
     /**
      * Whether to implement Access-Control-Allow-Origin: *
      * 
-     * @param allowAllOrigins if true, all origins are accepted and * is returned in the
header. Sections
-     *            5.1.1 and 5.1.2, and 5.2.1 and 5.2.2. If false, then the list of allowed
origins must be
+     * @param allowAllOrigins if true, all origins are accepted and 
+     * "*" is returned in the header. Sections
+     * 5.1.1 and 5.1.2, and 5.2.1 and 5.2.2. If false, then the list of allowed origins must
be
      */
     public void setAllowAllOrigins(boolean allowAllOrigins) {
         this.allowAllOrigins = allowAllOrigins;
@@ -403,19 +527,21 @@ public class CrossOriginResourceSharingF
         this.maxAge = maxAge;
     }
 
-    @SuppressWarnings("unchecked")
-    List<String> getHeadersFromInput(Message m, String key) {
-        Object obj = m.getExchange().get(key);
-        if (obj instanceof List<?>) {
-            return (List<String>)obj;
-        }
-        return null;
+
+    public boolean isDefaultOptionsMethodsHandlePreflight() {
+        return defaultOptionsMethodsHandlePreflight;
     }
 
-    private void addHeaders(ResponseBuilder rb, String key, List<String> vals) {
-        for (String v : vals) {
-            rb.header(key, v);
-        }
+    /**
+     * What to do when a preflight request comes along for a resource that has a handler
method for
+     * \@OPTIONS and there is no <tt>@{@link CrossResourceSharing}(localPreflight =
val)</tt>
+     * annotation on the method. If this is <tt>true</tt>, then the filter 
+     * defers to the resource class method.
+     * If this is false, then this filter performs preflight processing.
+     * @param defaultOptionsMethodsHandlePreflight true to defer to resource methods.
+     */
+    public void setDefaultOptionsMethodsHandlePreflight(boolean defaultOptionsMethodsHandlePreflight)
{
+        this.defaultOptionsMethodsHandlePreflight = defaultOptionsMethodsHandlePreflight;
     }
 
 }

Added: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java
URL: http://svn.apache.org/viewvc/cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java?rev=1209463&view=auto
==============================================================================
--- cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java
(added)
+++ cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java
Fri Dec  2 13:25:10 2011
@@ -0,0 +1,42 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.cxf.jaxrs.cors;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotate a JAX-RS class to provide pre-flight access control options 
+ * based on the CORS standard's definition of a resource for access 
+ * control purposes: a URL + method. Each @CrossScriptOrignResourceSharing
+ * annotation in here should contain a <tt>path</tt> attribute to define the

+ * path that it applies to. The <tt>allowedMethods</tt> attribute defines 
+ * the method or methods that the policy options apply to.
+ */
+@Target({ElementType.TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface CrossOriginResourceSharingPaths {
+    /**
+     * The individual annotations. 
+     */
+    CrossOriginResourceSharing[] value();
+}

Propchange: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: cxf/trunk/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/cors/CrossOriginResourceSharingPaths.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java
URL: http://svn.apache.org/viewvc/cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java?rev=1209463&r1=1209462&r2=1209463&view=diff
==============================================================================
--- cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java
(original)
+++ cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CrossOriginSimpleTest.java
Fri Dec  2 13:25:10 2011
@@ -21,6 +21,7 @@ package org.apache.cxf.systest.jaxrs.cor
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import org.apache.cxf.helpers.IOUtils;
@@ -36,6 +37,7 @@ import org.apache.http.client.ClientProt
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpDelete;
 import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpOptions;
 import org.apache.http.impl.client.DefaultHttpClient;
 
 import org.junit.Before;
@@ -82,14 +84,17 @@ public class CrossOriginSimpleTest exten
 
     private void assertAllOrigin(boolean allOrigins, String[] originList, String[] requestOrigins,
                                  boolean permitted) throws ClientProtocolException, IOException
{
-        connfigureAllowOrigins(allOrigins, originList);
+        configureAllowOrigins(allOrigins, originList);
 
         HttpClient httpclient = new DefaultHttpClient();
-        HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/test/simpleGet/HelloThere");
+        HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/untest/simpleGet/HelloThere");
         if (requestOrigins != null) {
+            StringBuffer ob = new StringBuffer();
             for (String requestOrigin : requestOrigins) {
-                httpget.addHeader("Origin", requestOrigin);
+                ob.append(requestOrigin);
+                ob.append(" "); // extra trailing space won't hurt.
             }
+            httpget.addHeader("Origin", ob.toString());
         }
         HttpResponse response = httpclient.execute(httpget);
         assertEquals(200, response.getStatusLine().getStatusCode());
@@ -110,9 +115,10 @@ public class CrossOriginSimpleTest exten
                 assertEquals("*", aaoHeaders[0].getValue());
             } else {
                 List<String> ovalues = headerValues(aaoHeaders);
-                assertEquals(requestOrigins.length, ovalues.size());
+                assertEquals(1, ovalues.size()); // get back one ac-allow-origin header.
+                String[] origins = ovalues.get(0).split(" +");
                 for (int x = 0; x < requestOrigins.length; x++) {
-                    assertEquals(requestOrigins[x], ovalues.get(x));
+                    assertEquals(requestOrigins[x], origins[x]);
                 }
             }
         } else {
@@ -121,7 +127,7 @@ public class CrossOriginSimpleTest exten
         }
     }
 
-    private void connfigureAllowOrigins(boolean allOrigins, String[] originList) {
+    private void configureAllowOrigins(boolean allOrigins, String[] originList) {
         if (allOrigins) {
             originList = new String[0];
         }
@@ -208,14 +214,12 @@ public class CrossOriginSimpleTest exten
         assertEquals("ok", r);
         
         HttpClient httpclient = new DefaultHttpClient();
-        HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/test/simpleGet/HelloThere");
+        HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/untest/simpleGet/HelloThere");
         httpget.addHeader("Origin", "http://localhost:" + PORT);
 
         HttpResponse response = httpclient.execute(httpget);
         assertEquals(200, response.getStatusLine().getStatusCode());
-        Header[] aaoHeaders = response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS);
-        assertEquals(1, aaoHeaders.length);
-        assertEquals("true", aaoHeaders[0].getValue());
+        assertAllowCredentials(response, true);
     }
     
     @Test
@@ -225,33 +229,79 @@ public class CrossOriginSimpleTest exten
         assertEquals("ok", r);
         
         HttpClient httpclient = new DefaultHttpClient();
-        HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/test/simpleGet/HelloThere");
+        HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/untest/simpleGet/HelloThere");
         httpget.addHeader("Origin", "http://localhost:" + PORT);
 
         HttpResponse response = httpclient.execute(httpget);
         assertEquals(200, response.getStatusLine().getStatusCode());
-        Header[] aaoHeaders = response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS);
-        assertEquals(1, aaoHeaders.length);
-        assertEquals("false", aaoHeaders[0].getValue());
+        assertAllowCredentials(response, false);
     }
     
     @Test
     public void testNonSimpleActualRequest() throws Exception {
-        connfigureAllowOrigins(true, null);
+        configureAllowOrigins(true, null);
         String r = configClient.replacePath("/setAllowCredentials/false")
             .accept("text/plain").post(null, String.class);
         assertEquals("ok", r);
         
         HttpClient httpclient = new DefaultHttpClient();
-        HttpDelete httpdelete = new HttpDelete("http://localhost:" + PORT + "/test/delete");
+        HttpDelete httpdelete = new HttpDelete("http://localhost:" + PORT + "/untest/delete");
         httpdelete.addHeader("Origin", "http://localhost:" + PORT);
 
         HttpResponse response = httpclient.execute(httpdelete);
         assertEquals(200, response.getStatusLine().getStatusCode());
+        assertAllowCredentials(response, false);
+        assertOriginResponse(true, null, true, response);
+    }
+
+    private void assertAllowCredentials(HttpResponse response, boolean correct) {
         Header[] aaoHeaders = response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_CREDENTIALS);
         assertEquals(1, aaoHeaders.length);
-        assertEquals("false", aaoHeaders[0].getValue());
-        assertOriginResponse(true, null, true, response);
+        assertEquals(Boolean.toString(correct), aaoHeaders[0].getValue());
+    }
+    
+    @Test
+    public void testAnnotatedSimple() throws Exception {
+        configureAllowOrigins(true, null);
+        String r = configClient.replacePath("/setAllowCredentials/false")
+            .accept("text/plain").post(null, String.class);
+        assertEquals("ok", r);
+        HttpClient httpclient = new DefaultHttpClient();
+        HttpGet httpget = new HttpGet("http://localhost:" + PORT + "/untest/annotatedGet/HelloThere");
+        // this is the origin we expect to get.
+        httpget.addHeader("Origin", "http://area51.mil:31415");
+        HttpResponse response = httpclient.execute(httpget);
+        assertEquals(200, response.getStatusLine().getStatusCode());
+        assertOriginResponse(false, new String[]{"http://area51.mil:31415"}, true, response);
+        assertAllowCredentials(response, false);
+        List<String> exposeHeadersValues 
+            = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS));
+        assertEquals(Arrays.asList(new String[] {"X-custom-3", "X-custom-4" }), exposeHeadersValues);
+    }
+    
+    @Test
+    public void testAnnotatedMethodPreflight() throws Exception {
+        configureAllowOrigins(true, null);
+        String r = configClient.replacePath("/setAllowCredentials/false")
+            .accept("text/plain").post(null, String.class);
+        assertEquals("ok", r);
+        HttpClient httpclient = new DefaultHttpClient();
+        HttpOptions http = new HttpOptions("http://localhost:" + PORT + "/untest/annotatedPut");
+        // this is the origin we expect to get.
+        http.addHeader("Origin", "http://area51.mil:31415");
+        http.addHeader(CorsHeaderConstants.HEADER_AC_REQUEST_METHOD, "PUT");
+        http.addHeader(CorsHeaderConstants.HEADER_AC_REQUEST_HEADERS, "X-custom-1, X-custom-2");
+        HttpResponse response = httpclient.execute(http);
+        assertEquals(200, response.getStatusLine().getStatusCode());
+        assertOriginResponse(false, new String[]{"http://area51.mil:31415"}, true, response);
+        assertAllowCredentials(response, false);
+        List<String> exposeHeadersValues 
+            = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_EXPOSE_HEADERS));
+        // depend on knowing the order.
+        assertEquals(Arrays.asList(new String[] {"X-custom-3", "X-custom-4" }), exposeHeadersValues);
+        List<String> allowHeadersValues 
+            = headerValues(response.getHeaders(CorsHeaderConstants.HEADER_AC_ALLOW_HEADERS));
+        assertEquals(Arrays.asList(new String[] {"X-custom-1", "X-custom-2" }), allowHeadersValues);
     }
 
     @Ignore

Copied: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java
(from r1209425, cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java)
URL: http://svn.apache.org/viewvc/cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java?p2=cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java&p1=cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java&r1=1209425&r2=1209463&rev=1209463&view=diff
==============================================================================
--- cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/CorsServer.java
(original)
+++ cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java
Fri Dec  2 13:25:10 2011
@@ -19,17 +19,31 @@
 
 package org.apache.cxf.systest.jaxrs.cors;
 
+import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Response;
 
+import org.apache.cxf.jaxrs.cors.CrossOriginResourceSharing;
+import org.apache.cxf.jaxrs.cors.CrossOriginResourceSharingPaths;
+
 /**
- * 
+ * Service bean with no class-level annotation for cross-script control.
  */
-public class CorsServer {
+@CrossOriginResourceSharingPaths(
+ @CrossOriginResourceSharing(path = "/annotatedPut", 
+     allowOrigins = { "http://area51.mil:31415" }, 
+     allowCredentials = true, 
+     maxAge = 1, 
+     allowMethods = { "PUT" },
+     allowHeaders = { "X-custom-1", "X-custom-2" },
+     exposeHeaders = {"X-custom-3", "X-custom-4" }
+ ))                                 
+public class UnannotatedCorsServer {
 
     @GET
     @Produces("text/plain")
@@ -37,10 +51,34 @@ public class CorsServer {
     public String simpleGet(@PathParam("echo") String echo) {
         return echo;
     }
-    
+
     @DELETE
     @Path("/delete")
     public Response deleteSomething() {
         return Response.ok().build();
     }
+    
+    @GET
+    @CrossOriginResourceSharing(allowOrigins = {
+            "http://area51.mil:31415" }, 
+             allowCredentials = true, 
+             exposeHeaders = {"X-custom-3", "X-custom-4" })
+    @Produces("text/plain")
+    @Path("/annotatedGet/{echo}")
+    public String annotatedGet(@PathParam("echo") String echo) {
+        return echo;
+    }
+    
+    /**
+     * A method annotated to test preflight.
+     * @param input
+     * @return
+     */
+    @PUT
+    @Consumes("text/plain")
+    @Produces("text/plain")
+    @Path("/annotatedPut")
+    public String annotatedPut(String input) {
+        return input;
+    }
 }

Propchange: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: cxf/trunk/systests/jaxrs/src/test/java/org/apache/cxf/systest/jaxrs/cors/UnannotatedCorsServer.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml
URL: http://svn.apache.org/viewvc/cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml?rev=1209463&r1=1209462&r2=1209463&view=diff
==============================================================================
--- cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml (original)
+++ cxf/trunk/systests/jaxrs/src/test/resources/jaxrs_cors/WEB-INF/beans.xml Fri Dec  2 13:25:10
2011
@@ -25,17 +25,17 @@ http://cxf.apache.org/core 
 		<property name="allowAllOrigins" value="true" />
 	</bean>
 
-	<jaxrs:server id="cors-service" address="/test">
+	<jaxrs:server id="cors-service" address="/untest">
 		<jaxrs:serviceBeans>
 			<ref bean="cors-server" />
 		</jaxrs:serviceBeans>
 		<jaxrs:providers>
 			<ref bean="cors-filter" />
-		</jaxrs:providers><!-- 
+		</jaxrs:providers>
 		<jaxrs:features>
 			<cxf:logging />
 		</jaxrs:features>
-		-->
+
 	</jaxrs:server>
 	<jaxrs:server id="config-service" address="/config">
 		<jaxrs:serviceBeans>
@@ -50,5 +50,5 @@ http://cxf.apache.org/core 
 		<property name='inputFilter' ref='cors-filter'/>
 	</bean>
 	<bean id="cors-server" scope="prototype"
-		class="org.apache.cxf.systest.jaxrs.cors.CorsServer" />
+		class="org.apache.cxf.systest.jaxrs.cors.UnannotatedCorsServer" />
 </beans>



Mime
View raw message