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>


Reply via email to