From 3b2c6057e7d8215b42c5639ec7062df44ee17ff7 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 19 Apr 2014 17:46:59 +0200 Subject: [PATCH] WW-4404 Implements HttpInterceptor --- .../xwork2/util/AnnotationUtils.java | 27 +- .../httpmethod/AllowedHttpMethod.java | 38 +++ .../interceptor/httpmethod/HttpDelete.java | 38 +++ .../interceptor/httpmethod/HttpGet.java | 38 +++ .../interceptor/httpmethod/HttpGetOrPost.java | 38 +++ .../interceptor/httpmethod/HttpMethod.java | 43 ++++ .../httpmethod/HttpMethodAware.java | 49 ++++ .../httpmethod/HttpMethodInterceptor.java | 156 ++++++++++++ .../interceptor/httpmethod/HttpPost.java | 38 +++ .../interceptor/httpmethod/HttpPut.java | 38 +++ core/src/main/resources/struts-default.xml | 4 + .../apache/struts2/HttpMethodsTestAction.java | 82 ++++++ .../httpmethod/HttpMethodInterceptorTest.java | 234 ++++++++++++++++++ .../httpmethod/HttpMethodTest.java | 58 +++++ 14 files changed, 877 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/AllowedHttpMethod.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpDelete.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpGet.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpGetOrPost.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethod.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodAware.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpPost.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpPut.java create mode 100644 core/src/test/java/org/apache/struts2/HttpMethodsTestAction.java create mode 100644 core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java create mode 100644 core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodTest.java diff --git a/core/src/main/java/com/opensymphony/xwork2/util/AnnotationUtils.java b/core/src/main/java/com/opensymphony/xwork2/util/AnnotationUtils.java index cbebc8003..2ab1f9192 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/AnnotationUtils.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/AnnotationUtils.java @@ -18,9 +18,11 @@ */ package com.opensymphony.xwork2.util; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ClassUtils; import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; @@ -52,7 +54,7 @@ public class AnnotationUtils { * @param clazz The {@link Class} to inspect * @param allFields list of all fields */ - public static void addAllFields(Class annotationClass, Class clazz, List allFields) { + public static void addAllFields(Class annotationClass, Class clazz, List allFields) { if (clazz == null) { return; @@ -76,7 +78,7 @@ public class AnnotationUtils { * @param clazz The {@link Class} to inspect * @param allMethods list of all methods */ - public static void addAllMethods(Class annotationClass, Class clazz, List allMethods) { + public static void addAllMethods(Class annotationClass, Class clazz, List allMethods) { if (clazz == null) { return; @@ -97,12 +99,12 @@ public class AnnotationUtils { * @param clazz The {@link Class} to inspect * @param allInterfaces list of all interfaces */ - public static void addAllInterfaces(Class clazz, List allInterfaces) { + public static void addAllInterfaces(Class clazz, List> allInterfaces) { if (clazz == null) { return; } - Class[] interfaces = clazz.getInterfaces(); + Class[] interfaces = clazz.getInterfaces(); allInterfaces.addAll(Arrays.asList(interfaces)); addAllInterfaces(clazz.getSuperclass(), allInterfaces); } @@ -189,4 +191,21 @@ public class AnnotationUtils { return anns; } + + /** + * Varargs version of AnnotatedElement.isAnnotationPresent() + * + * @see AnnotatedElement + */ + @SafeVarargs + public static boolean isAnnotatedBy(AnnotatedElement annotatedElement, Class... annotation) { + if (ArrayUtils.isEmpty(annotation)) return false; + + for (Class c : annotation) { + if (annotatedElement.isAnnotationPresent(c)) return true; + } + + return false; + } + } diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/AllowedHttpMethod.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/AllowedHttpMethod.java new file mode 100644 index 000000000..ca7c4d3bb --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/AllowedHttpMethod.java @@ -0,0 +1,38 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to limit with what http method action or action's method can be called + * + * @see HttpMethodInterceptor + * @since 6.2.0 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AllowedHttpMethod { + + HttpMethod[] value() default {}; + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpDelete.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpDelete.java new file mode 100644 index 000000000..08f87922a --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpDelete.java @@ -0,0 +1,38 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to allow call action or action's method via DELETE request only + * + * @see org.apache.struts2.interceptor.httpmethod.HttpMethodInterceptor + * @since 6.2.0 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HttpDelete { + + HttpMethod[] value() default { HttpMethod.DELETE }; + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpGet.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpGet.java new file mode 100644 index 000000000..8809905b1 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpGet.java @@ -0,0 +1,38 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to allow call action or action's method via GET request only + * + * @see HttpMethodInterceptor + * @since 6.2.0 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HttpGet { + + HttpMethod[] value() default { HttpMethod.GET }; + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpGetOrPost.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpGetOrPost.java new file mode 100644 index 000000000..f7ace5d78 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpGetOrPost.java @@ -0,0 +1,38 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to allow call action or action's method via GET or POST request only + * + * @see HttpMethodInterceptor + * @since 6.2.0 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HttpGetOrPost { + + HttpMethod[] value() default { HttpMethod.GET, HttpMethod.POST }; + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethod.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethod.java new file mode 100644 index 000000000..8e9b85099 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethod.java @@ -0,0 +1,43 @@ +/* + * 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.struts2.interceptor.httpmethod; + +/** + * Enum represents possible http request types + * + * @see HttpMethodInterceptor + * @since 6.2.0 + */ +public enum HttpMethod { + + GET, + HEAD, + POST, + PUT, + DELETE, + TRACE, + OPTIONS, + CONNECT, + PATCH; + + public static HttpMethod parse(String httpRequestMethod) { + return valueOf(httpRequestMethod.toUpperCase()); + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodAware.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodAware.java new file mode 100644 index 000000000..95f3bd106 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodAware.java @@ -0,0 +1,49 @@ +/* + * 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.struts2.interceptor.httpmethod; + +/** + * Action when implements this interface is notified about what method was used to perform request, + * it works in connection with {@link HttpMethodInterceptor} + * + * Another function of this interface is to return result, which should be returned when action + * was called with wrong http method + * + * @see HttpMethodInterceptor + * @since 6.2.0 + */ +public interface HttpMethodAware { + + /** + * Notifies action about http method used to perform request + * + * @param httpMethod {@link javax.servlet.http.HttpServletRequest#getMethod()} translated to enum + */ + void setMethod(HttpMethod httpMethod); + + /** + * Action name to use when action was requested with wrong http method + * can return null and then default result name will be used instead defined + * in {@link HttpMethodInterceptor} + * + * @return result name or null + */ + String getBadRequestResultName(); + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java new file mode 100644 index 000000000..c0afca1c3 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptor.java @@ -0,0 +1,156 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import com.opensymphony.xwork2.ActionInvocation; +import com.opensymphony.xwork2.interceptor.AbstractInterceptor; +import com.opensymphony.xwork2.util.AnnotationUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.servlet.http.HttpServletRequest; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Interceptor is used to control with what http methods action can be called, + * if request with not allowed method was performed, {@link #badRequestResultName} + * will be returned or if action implements {@link HttpMethodAware} + * and {@link HttpMethodAware#getBadRequestResultName()} returns non-null result name, + * thus value will be used instead. + *

+ * To limit allowed http methods, annotate action class with {@link AllowedHttpMethod} and specify, + * which methods are allowed. You can also use shorter versions {@link HttpGet}, {@link HttpPost}, + * {@link HttpPut}, {@link HttpDelete} and {@link HttpGetOrPost} + *

+ * You can combine any of these annotations to achieve required allowed methods' filtering. + * + * @see HttpMethodAware + * @see HttpMethod + * @see AllowedHttpMethod + * @see HttpGet + * @see HttpPost + * @see HttpPut + * @see HttpDelete + * @see HttpGetOrPost + * @since 6.2.0 + */ +public class HttpMethodInterceptor extends AbstractInterceptor { + + @SuppressWarnings({"unchecked"}) + private static final Class[] HTTP_METHOD_ANNOTATIONS = new Class[]{ + AllowedHttpMethod.class, + HttpGet.class, + HttpPost.class, + HttpGetOrPost.class, + HttpPut.class, + HttpDelete.class + }; + + private static final Logger LOG = LogManager.getLogger(HttpMethodInterceptor.class); + + private String badRequestResultName = "bad-request"; + + @Override + public String intercept(ActionInvocation invocation) throws Exception { + Object action = invocation.getAction(); + HttpServletRequest request = invocation.getInvocationContext().getServletRequest(); + + if (action instanceof HttpMethodAware) { + LOG.debug("Action: {} implements: {}, setting request method: {}", + action, HttpMethodAware.class.getSimpleName(), request.getMethod()); + ((HttpMethodAware) (action)).setMethod(HttpMethod.parse(request.getMethod())); + } + + if (invocation.getProxy().isMethodSpecified()) { + Method method = action.getClass().getMethod(invocation.getProxy().getMethod()); + if (AnnotationUtils.isAnnotatedBy(method, HTTP_METHOD_ANNOTATIONS)) { + LOG.debug("Action's method: {} annotated with: {}, checking if request: {} meets allowed methods!", + invocation.getProxy().getMethod(), AllowedHttpMethod.class.getSimpleName(), request.getMethod()); + return doIntercept(invocation, method); + } + } else if (AnnotationUtils.isAnnotatedBy(action.getClass(), HTTP_METHOD_ANNOTATIONS)) { + LOG.debug("Action: {} annotated with: {}, checking if request: {} meets allowed methods!", + action, AllowedHttpMethod.class.getSimpleName(), request.getMethod()); + return doIntercept(invocation, action.getClass()); + } + + return invocation.invoke(); + } + + protected String doIntercept(ActionInvocation invocation, AnnotatedElement element) throws Exception { + List allowedMethods = readAllowedMethods(element); + HttpServletRequest request = invocation.getInvocationContext().getServletRequest(); + HttpMethod requestedMethod = HttpMethod.parse(request.getMethod()); + + if (allowedMethods.contains(requestedMethod)) { + LOG.trace("Request method: {} matches allowed methods: {}, continuing invocation!", requestedMethod, allowedMethods); + return invocation.invoke(); + } else { + LOG.trace("Request method: {} doesn't match allowed methods: {}, continuing invocation!", requestedMethod, allowedMethods); + return getBadRequestResultName(invocation); + } + } + + protected List readAllowedMethods(AnnotatedElement element) { + List allowedMethods = new ArrayList<>(); + if (AnnotationUtils.isAnnotatedBy(element, AllowedHttpMethod.class)) { + allowedMethods.addAll(Arrays.asList(element.getAnnotation(AllowedHttpMethod.class).value())); + } + if (AnnotationUtils.isAnnotatedBy(element, HttpGet.class)) { + allowedMethods.addAll(Arrays.asList(element.getAnnotation(HttpGet.class).value())); + } + if (AnnotationUtils.isAnnotatedBy(element, HttpPost.class)) { + allowedMethods.addAll(Arrays.asList(element.getAnnotation(HttpPost.class).value())); + } + if (AnnotationUtils.isAnnotatedBy(element, HttpPut.class)) { + allowedMethods.addAll(Arrays.asList(element.getAnnotation(HttpPut.class).value())); + } + if (AnnotationUtils.isAnnotatedBy(element, HttpDelete.class)) { + allowedMethods.addAll(Arrays.asList(element.getAnnotation(HttpDelete.class).value())); + } + if (AnnotationUtils.isAnnotatedBy(element, HttpGetOrPost.class)) { + allowedMethods.addAll(Arrays.asList(element.getAnnotation(HttpGetOrPost.class).value())); + } + return Collections.unmodifiableList(allowedMethods); + } + + protected String getBadRequestResultName(ActionInvocation invocation) { + Object action = invocation.getAction(); + String resultName = badRequestResultName; + if (action instanceof HttpMethodAware) { + String actionResultName = ((HttpMethodAware) action).getBadRequestResultName(); + if (actionResultName != null) { + resultName = actionResultName; + } + } + LOG.trace("Bad request result name is: {}", resultName); + return resultName; + } + + public void setBadRequestResultName(String badRequestResultName) { + this.badRequestResultName = badRequestResultName; + } + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpPost.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpPost.java new file mode 100644 index 000000000..667596be5 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpPost.java @@ -0,0 +1,38 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to allow call action or action's method via POST request only + * + * @see HttpMethodInterceptor + * @since 6.2.0 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HttpPost { + + HttpMethod[] value() default { HttpMethod.POST }; + +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpPut.java b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpPut.java new file mode 100644 index 000000000..684837da3 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/httpmethod/HttpPut.java @@ -0,0 +1,38 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to allow call action or action's method via PUT request only + * + * @see org.apache.struts2.interceptor.httpmethod.HttpMethodInterceptor + * @since 6.2.0 + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface HttpPut { + + HttpMethod[] value() default { HttpMethod.PUT }; + +} diff --git a/core/src/main/resources/struts-default.xml b/core/src/main/resources/struts-default.xml index aec2e0278..90657630e 100644 --- a/core/src/main/resources/struts-default.xml +++ b/core/src/main/resources/struts-default.xml @@ -89,6 +89,7 @@ + @@ -99,6 +100,7 @@ + @@ -160,6 +162,7 @@ + @@ -191,6 +194,7 @@ + false diff --git a/core/src/test/java/org/apache/struts2/HttpMethodsTestAction.java b/core/src/test/java/org/apache/struts2/HttpMethodsTestAction.java new file mode 100644 index 000000000..255044b95 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/HttpMethodsTestAction.java @@ -0,0 +1,82 @@ +/* + * 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.struts2; + +import com.opensymphony.xwork2.ActionSupport; +import org.apache.struts2.interceptor.httpmethod.AllowedHttpMethod; +import org.apache.struts2.interceptor.httpmethod.HttpDelete; +import org.apache.struts2.interceptor.httpmethod.HttpGet; +import org.apache.struts2.interceptor.httpmethod.HttpGetOrPost; +import org.apache.struts2.interceptor.httpmethod.HttpMethod; +import org.apache.struts2.interceptor.httpmethod.HttpMethodAware; +import org.apache.struts2.interceptor.httpmethod.HttpPost; +import org.apache.struts2.interceptor.httpmethod.HttpPut; + +import static org.apache.struts2.interceptor.httpmethod.HttpMethod.POST; + +@AllowedHttpMethod(POST) +public class HttpMethodsTestAction extends ActionSupport implements HttpMethodAware { + + private String resultName = null; + private HttpMethod httpMethod; + + public HttpMethodsTestAction() { + } + + public HttpMethodsTestAction(String resultName) { + this.resultName = resultName; + } + + @HttpGet + public String onGetOnly() { + return "onGetOnly"; + } + + @HttpPost + public String onPostOnly() { + return "onPostOnly"; + } + + @HttpGetOrPost + public String onGetPostOnly() { + return "onGetPostOnly"; + } + + @HttpPut @HttpPost + public String onPutOrPost() { + return "onPutOrPost"; + } + + @HttpDelete + public String onDelete() { + return "onDelete"; + } + + public void setMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + } + + public HttpMethod getHttpMethod() { + return httpMethod; + } + + public String getBadRequestResultName() { + return resultName; + } +} diff --git a/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java new file mode 100644 index 000000000..98e78c3fa --- /dev/null +++ b/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodInterceptorTest.java @@ -0,0 +1,234 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.mock.MockActionInvocation; +import com.opensymphony.xwork2.mock.MockActionProxy; +import org.apache.struts2.HttpMethodsTestAction; +import org.apache.struts2.StrutsInternalTestCase; +import org.apache.struts2.TestAction; +import org.springframework.mock.web.MockHttpServletRequest; + +public class HttpMethodInterceptorTest extends StrutsInternalTestCase { + + private HttpMethodInterceptor interceptor; + private MockActionInvocation invocation; + private MockActionProxy actionProxy; + + public void testNotAnnotatedAction() throws Exception { + // given + prepareActionInvocation(new TestAction()); + invocation.setResultCode("success"); + + prepareRequest("post"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("success", resultName); + } + + public void testActionWithPostAllowed() throws Exception { + // given + prepareActionInvocation(new HttpMethodsTestAction()); + invocation.setResultCode("success"); + + prepareRequest("post"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("success", resultName); + } + + public void testGetIsNotAllowed() throws Exception { + // given + prepareActionInvocation(new HttpMethodsTestAction()); + invocation.setResultCode("success"); + + prepareRequest("get"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("bad-request", resultName); + } + + public void testGetIsNotAllowedWithCustomResultName() throws Exception { + // given + prepareActionInvocation(new HttpMethodsTestAction()); + interceptor.setBadRequestResultName("custom-bad-request"); + invocation.setResultCode("success"); + + prepareRequest("get"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("custom-bad-request", resultName); + } + + public void testGetIsNotAllowedWithActionDefinedResultName() throws Exception { + // given + prepareActionInvocation(new HttpMethodsTestAction("action-bad-request")); + interceptor.setBadRequestResultName("custom-bad-request"); + invocation.setResultCode("success"); + + prepareRequest("get"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("action-bad-request", resultName); + } + + public void testGetOnlyOnMethod() throws Exception { + // given + HttpMethodsTestAction action = new HttpMethodsTestAction(); + prepareActionInvocation(action); + actionProxy.setMethod("onGetOnly"); + actionProxy.setMethodSpecified(true); + invocation.setResultCode("onGetOnly"); + + prepareRequest("get"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("onGetOnly", resultName); + assertEquals(HttpMethod.GET, action.getHttpMethod()); + } + + public void testPostOnlyOnMethod() throws Exception { + // given + HttpMethodsTestAction action = new HttpMethodsTestAction(); + prepareActionInvocation(action); + actionProxy.setMethod("onPostOnly"); + actionProxy.setMethodSpecified(true); + + invocation.setResultCode("onPostOnly"); + + prepareRequest("post"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("onPostOnly", resultName); + assertEquals(HttpMethod.POST, action.getHttpMethod()); + } + + public void testGetPostOnlyOnMethod() throws Exception { + // given + HttpMethodsTestAction action = new HttpMethodsTestAction(); + prepareActionInvocation(action); + actionProxy.setMethod("onGetPostOnly"); + actionProxy.setMethodSpecified(true); + + invocation.setResultCode("onGetPostOnly"); + + prepareRequest("post"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("onGetPostOnly", resultName); + assertEquals(HttpMethod.POST, action.getHttpMethod()); + } + + public void testDeleteOnMethod() throws Exception { + // given + HttpMethodsTestAction action = new HttpMethodsTestAction(); + prepareActionInvocation(action); + actionProxy.setMethod("onDelete"); + actionProxy.setMethodSpecified(true); + + invocation.setResultCode("onDelete"); + + prepareRequest("DELETE"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("onDelete", resultName); + assertEquals(HttpMethod.DELETE, action.getHttpMethod()); + } + + public void testPutOnPutOrPostMethod() throws Exception { + // given + HttpMethodsTestAction action = new HttpMethodsTestAction(); + prepareActionInvocation(action); + actionProxy.setMethod("onPutOrPost"); + actionProxy.setMethodSpecified(true); + + invocation.setResultCode("onPutOrPost"); + + prepareRequest("PUT"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("onPutOrPost", resultName); + assertEquals(HttpMethod.PUT, action.getHttpMethod()); + } + + public void testPostOnPutOrPostMethod() throws Exception { + // given + HttpMethodsTestAction action = new HttpMethodsTestAction(); + prepareActionInvocation(action); + actionProxy.setMethod("onPutOrPost"); + actionProxy.setMethodSpecified(true); + + invocation.setResultCode("onPutOrPost"); + + prepareRequest("POST"); + + // when + String resultName = interceptor.intercept(invocation); + + // then + assertEquals("onPutOrPost", resultName); + assertEquals(HttpMethod.POST, action.getHttpMethod()); + } + + private void prepareActionInvocation(Object action) { + interceptor = new HttpMethodInterceptor(); + invocation = new MockActionInvocation(); + invocation.setAction(action); + actionProxy = new MockActionProxy(); + invocation.setProxy(actionProxy); + } + + private void prepareRequest(String httpMethod) { + MockHttpServletRequest request = new MockHttpServletRequest(httpMethod, "/action"); + ActionContext.getContext().withServletRequest(request); + invocation.setInvocationContext(ActionContext.getContext()); + } + +} diff --git a/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodTest.java b/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodTest.java new file mode 100644 index 000000000..c56ef5f7a --- /dev/null +++ b/core/src/test/java/org/apache/struts2/interceptor/httpmethod/HttpMethodTest.java @@ -0,0 +1,58 @@ +/* + * 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.struts2.interceptor.httpmethod; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class HttpMethodTest { + + @Test + public void shouldConvertHttpRequestMethod_toProperEnum() { + // given + String httpRequestMethod = "post"; + + // when + HttpMethod httpMethod = HttpMethod.parse(httpRequestMethod); + + // then + assertEquals(HttpMethod.POST, httpMethod); + } + + @Test + public void shouldValueOfThrowsException() { + // given + String httpRequestMethod = "post"; + + // when + Throwable expected = null; + try { + HttpMethod.valueOf(httpRequestMethod); + } catch (IllegalArgumentException e) { + expected = e; + } + + // then + assertNotNull(expected); + assertEquals(expected.getClass(), IllegalArgumentException.class); + } + +}