diff --git a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresAuthentication.java b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresAuthentication.java new file mode 100644 index 0000000000..4e11420bd2 --- /dev/null +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresAuthentication.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010-2013 eBusiness Information, Excilys Group + * + * Licensed 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.androidannotations.annotations.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresAuthentication { + +} diff --git a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresCookie.java b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresCookie.java new file mode 100644 index 0000000000..0135048dab --- /dev/null +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresCookie.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010-2013 eBusiness Information, Excilys Group + * + * Licensed 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.androidannotations.annotations.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresCookie { + public String[] value(); +} diff --git a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresCookieInUrl.java b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresCookieInUrl.java new file mode 100644 index 0000000000..30291a5e5f --- /dev/null +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresCookieInUrl.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010-2013 eBusiness Information, Excilys Group + * + * Licensed 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.androidannotations.annotations.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresCookieInUrl { + public String[] value(); +} diff --git a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresHeader.java b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresHeader.java new file mode 100644 index 0000000000..4867293575 --- /dev/null +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/RequiresHeader.java @@ -0,0 +1,27 @@ +/** + * Copyright (C) 2010-2013 eBusiness Information, Excilys Group + * + * Licensed 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.androidannotations.annotations.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface RequiresHeader { + String[] value(); +} diff --git a/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/SetsCookie.java b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/SetsCookie.java new file mode 100644 index 0000000000..03189e526c --- /dev/null +++ b/AndroidAnnotations/androidannotations-api/src/main/java/org/androidannotations/annotations/rest/SetsCookie.java @@ -0,0 +1,28 @@ +/** + * Copyright (C) 2010-2013 eBusiness Information, Excilys Group + * + * Licensed 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.androidannotations.annotations.rest; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.CLASS) +@Target({ ElementType.METHOD, ElementType.TYPE }) +public @interface SetsCookie { + + public String[] value(); +} diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/RestAnnotationHelper.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/RestAnnotationHelper.java index be00303100..e7f2933bf3 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/RestAnnotationHelper.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/helper/RestAnnotationHelper.java @@ -26,6 +26,7 @@ import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.VariableElement; +import org.androidannotations.processing.rest.MethodProcessor; import org.androidannotations.validation.IsValid; public class RestAnnotationHelper extends TargetAnnotationHelper { @@ -43,6 +44,13 @@ public void urlVariableNamesExistInParameters(ExecutableElement element, Set ANDROID_FRAGMENT_QUALIFIED_NAMES = asList(CanonicalNameConstants.FRAGMENT, CanonicalNameConstants.SUPPORT_V4_FRAGMENT); private static final String METHOD_NAME_SET_ROOT_URL = "setRootUrl"; + private static final String METHOD_NAME_SET_AUTHENTICATION = "setAuthentication"; + private static final String METHOD_NAME_GET_COOKIE = "getCookie"; + private static final String METHOD_NAME_GET_HEADER = "getHeader"; private static final String METHOD_NAME_GET_ROOT_URL = "getRootUrl"; @@ -781,7 +784,10 @@ public void unannotatedMethodReturnsRestTemplate(TypeElement typeElement, IsVali List enclosedElements = typeElement.getEnclosedElements(); boolean foundGetRestTemplateMethod = false; boolean foundSetRestTemplateMethod = false; + boolean foundSetAuthenticationMethod = false; boolean foundSetRootUrlMethod = false; + boolean foundGetCookieMethod = false; + boolean foundGetHeaderMethod = false; boolean foundGetRootUrlMethod = false; for (Element enclosedElement : enclosedElements) { @@ -847,15 +853,46 @@ public void unannotatedMethodReturnsRestTemplate(TypeElement typeElement, IsVali } } else if (executableElement.getSimpleName().toString().equals(METHOD_NAME_SET_ROOT_URL) && !foundSetRootUrlMethod) { foundSetRootUrlMethod = true; + } else if (executableElement.getSimpleName().toString().equals(METHOD_NAME_SET_AUTHENTICATION) && !foundSetAuthenticationMethod) { + foundSetAuthenticationMethod = true; } else { valid.invalidate(); annotationHelper.printError(enclosedElement, "The method to set a RestTemplate should have only one RestTemplate parameter on a " + TargetAnnotationHelper.annotationName(Rest.class) + " annotated interface"); } + } else if (parameters.size() == 2) { + VariableElement firstParameter = parameters.get(0); + VariableElement secondParameter = parameters.get(1); + if (!(firstParameter.asType().toString().equals(CanonicalNameConstants.STRING) && secondParameter.asType().toString().equals(CanonicalNameConstants.STRING))) { + valid.invalidate(); + annotationHelper.printError(enclosedElement, "The method to set headers, cookies, or HTTP Basic Auth should have only String parameters on a " + TargetAnnotationHelper.annotationName(Rest.class) + " annotated interface"); + } } else { valid.invalidate(); annotationHelper.printError(enclosedElement, "The method to set a RestTemplate should have only one RestTemplate parameter on a " + TargetAnnotationHelper.annotationName(Rest.class) + " annotated interface"); } + } else if (returnType.toString().equals(CanonicalNameConstants.STRING)) { + List parameters = executableElement.getParameters(); + if (parameters.size() == 1) { + VariableElement firstParameter = parameters.get(0); + if (firstParameter.asType().toString().equals(CanonicalNameConstants.STRING)) { + if (executableElement.getSimpleName().toString().equals(METHOD_NAME_GET_COOKIE) && !foundGetCookieMethod) { + foundGetCookieMethod = true; + } else if (executableElement.getSimpleName().toString().equals(METHOD_NAME_GET_HEADER) && !foundGetHeaderMethod) { + foundGetHeaderMethod = true; + } else { + valid.invalidate(); + annotationHelper.printError(enclosedElement, "Only one getCookie(String) and one getHeader(String) method are allowed on a " + TargetAnnotationHelper.annotationName(Rest.class) + " annotated interface"); + } + } else { + valid.invalidate(); + annotationHelper.printError(enclosedElement, "Only getCookie(String) and getHeader(String) can return a String on a " + TargetAnnotationHelper.annotationName(Rest.class) + " annotated interface"); + } + + } else { + valid.invalidate(); + annotationHelper.printError(enclosedElement, "The only methods that can return a String on a " + TargetAnnotationHelper.annotationName(Rest.class) + " annotated interface are getCookie(String) and getHeader(String)"); + } } else { valid.invalidate(); annotationHelper.printError(enclosedElement, "All methods should be annotated in a " + TargetAnnotationHelper.annotationName(Rest.class) + " annotated interface, except the ones that returns or set a RestTemplate"); diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/GetPostProcessor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/GetPostProcessor.java index 1afac17926..7b255d9637 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/GetPostProcessor.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/GetPostProcessor.java @@ -42,6 +42,7 @@ import com.sun.codemodel.JCodeModel; import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JExpr; +import com.sun.codemodel.JExpression; import com.sun.codemodel.JInvocation; import com.sun.codemodel.JPackage; @@ -267,7 +268,7 @@ protected JInvocation addResponseEntityArg(JInvocation restCall, MethodProcessor } @Override - protected JInvocation addResultCallMethod(JInvocation restCall, MethodProcessorHolder methodHolder) { + protected JExpression addResultCallMethod(JExpression restCall, MethodProcessorHolder methodHolder) { JClass generatedReturnType = methodHolder.getMethodReturnClass(); if (generatedReturnType == null) { return restCall; diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/GetProcessor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/GetProcessor.java index d1c0e766a3..9c88d284e3 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/GetProcessor.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/GetProcessor.java @@ -54,7 +54,7 @@ protected JExpression generateHttpEntityVar(MethodProcessorHolder methodHolder) JClass httpEntity = holder.refClass(CanonicalNameConstants.HTTP_ENTITY); JBlock body = methodHolder.getBody(); - JVar httpHeadersVar = generateHttpHeadersVar(holder, body, executableElement); + JVar httpHeadersVar = generateHttpHeadersVar(methodHolder, holder, body, executableElement); boolean hasHeaders = httpHeadersVar != null; diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/HeadProcessor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/HeadProcessor.java index 5770b75838..203fb700a0 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/HeadProcessor.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/HeadProcessor.java @@ -26,7 +26,7 @@ import com.sun.codemodel.JClass; import com.sun.codemodel.JCodeModel; import com.sun.codemodel.JExpr; -import com.sun.codemodel.JInvocation; +import com.sun.codemodel.JExpression; public class HeadProcessor extends MethodProcessor { @@ -55,7 +55,7 @@ public void process(Element element, JCodeModel codeModel, EBeanHolder holder) t } @Override - protected JInvocation addResultCallMethod(JInvocation restCall, MethodProcessorHolder methodHolder) { + protected JExpression addResultCallMethod(JExpression restCall, MethodProcessorHolder methodHolder) { return JExpr.invoke(restCall, "getHeaders"); } diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/MethodProcessor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/MethodProcessor.java index a357fe98be..c6d9e9aedf 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/MethodProcessor.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/MethodProcessor.java @@ -27,17 +27,24 @@ import javax.lang.model.element.VariableElement; import org.androidannotations.annotations.rest.Accept; +import org.androidannotations.annotations.rest.RequiresAuthentication; +import org.androidannotations.annotations.rest.RequiresCookie; +import org.androidannotations.annotations.rest.RequiresCookieInUrl; +import org.androidannotations.annotations.rest.RequiresHeader; +import org.androidannotations.annotations.rest.SetsCookie; import org.androidannotations.helper.APTCodeModelHelper; import org.androidannotations.helper.CanonicalNameConstants; import org.androidannotations.helper.RestAnnotationHelper; import org.androidannotations.processing.DecoratingElementProcessor; import org.androidannotations.processing.EBeanHolder; +import com.sun.codemodel.JArray; import com.sun.codemodel.JBlock; import com.sun.codemodel.JClass; import com.sun.codemodel.JCodeModel; import com.sun.codemodel.JExpr; import com.sun.codemodel.JExpression; +import com.sun.codemodel.JForEach; import com.sun.codemodel.JInvocation; import com.sun.codemodel.JMethod; import com.sun.codemodel.JMod; @@ -84,11 +91,17 @@ protected void generateRestTemplateCallBlock(MethodProcessorHolder methodHolder) // RestTemplate exchange() method call JInvocation restCall = JExpr.invoke(holder.restTemplateField, "exchange"); - // RestTemplate exchange() 1st arg : concat root url + suffix - JInvocation concatCall = JExpr.invoke(holder.rootUrlField, "concat"); + final String urlSuffix = methodHolder.getUrlSuffix(); + if (!(urlSuffix.startsWith("http://") || urlSuffix.startsWith("https://"))) { + // RestTemplate exchange() 1st arg : concat root url + suffix + JInvocation concatCall = JExpr.invoke(holder.rootUrlField, "concat"); - // RestTemplate exchange() 2nd arg : add url param - restCall.arg(concatCall.arg(JExpr.lit(methodHolder.getUrlSuffix()))); + // RestTemplate exchange() 2nd arg : add url param + restCall.arg(concatCall.arg(JExpr.lit(urlSuffix))); + } else { + // full url provided... don't prefix + restCall.arg(JExpr.lit(urlSuffix)); + } // RestTemplate exchange() 3rd arg : add HttpMethod type param JClass httpMethod = eBeanHolder.refClass(CanonicalNameConstants.HTTP_METHOD); @@ -99,7 +112,7 @@ protected void generateRestTemplateCallBlock(MethodProcessorHolder methodHolder) restCall.arg(httpMethod.staticRef(restMethodInCapitalLetters)); - JVar hashMapVar = generateHashMapVar(methodHolder); + JVar hashMapVar = generateHashMapVar(holder, methodHolder); restCall = addHttpEntityVar(restCall, methodHolder); restCall = addResponseEntityArg(restCall, methodHolder); @@ -108,7 +121,60 @@ protected void generateRestTemplateCallBlock(MethodProcessorHolder methodHolder) restCall.arg(hashMapVar); } - insertRestCallInBody(body, restCall, methodHolder, methodReturnVoid); + final JExpression result; + final boolean usesInstance; // do we have an instance of the entity? + + // attempt to retrieve cookies from the response + String[] settingCookies = retrieveSettingCookieNames(executableElement); + boolean setsCookies = settingCookies != null; + if (setsCookies) { + + JClass voidClass = eBeanHolder.refClass(Void.class); + JClass responseEntityClass = eBeanHolder.refClass(CanonicalNameConstants.RESPONSE_ENTITY).narrow(methodReturnVoid ? voidClass : expectedClass); + JVar responseEntity = body.decl(responseEntityClass, "response", restCall); + + // set cookies + JClass listClass = eBeanHolder.refClass(List.class).narrow(String.class); + JClass stringClass = eBeanHolder.refClass(CanonicalNameConstants.STRING); + JClass stringArrayClass = stringClass.array(); + JArray cookiesArray = JExpr.newArray(stringClass); + for (String cookie : settingCookies) { + cookiesArray.add(JExpr.lit(cookie)); + } + JVar requestedCookiesVar = body.decl(stringArrayClass, "requestedCookies", cookiesArray); + + JInvocation setCookiesList = JExpr.invoke(responseEntity, "getHeaders").invoke("get").arg("Set-Cookie"); + JVar allCookiesList = body.decl(listClass, "allCookies", setCookiesList); + + // for loop over list... add if in string array + JForEach forEach = body.forEach(stringClass, "rawCookie", allCookiesList); + JVar rawCookieVar = forEach.var(); + + JBlock forLoopBody = forEach.body(); + + JForEach innerForEach = forLoopBody.forEach(stringClass, "thisCookieName", requestedCookiesVar); + JBlock innerBody = innerForEach.body(); + JBlock thenBlock = innerBody._if(JExpr.invoke(rawCookieVar, "startsWith").arg(innerForEach.var()))._then(); + + // where does the cookie VALUE end? + JInvocation valueEnd = rawCookieVar.invoke("indexOf").arg(JExpr.lit(';')); + JVar valueEndVar = thenBlock.decl(methodHolder.getCodeModel().INT, "valueEnd", valueEnd); + JBlock fixValueEndBlock = thenBlock._if(valueEndVar.eq(JExpr.lit(-1)))._then(); + fixValueEndBlock.assign(valueEndVar, rawCookieVar.invoke("length")); + + JExpression indexOfValue = rawCookieVar.invoke("indexOf").arg("=").plus(JExpr.lit(1)); + JInvocation cookieValue = rawCookieVar.invoke("substring").arg(indexOfValue).arg(valueEndVar); + thenBlock.invoke(holder.availableCookiesField, "put").arg(innerForEach.var()).arg(cookieValue); + thenBlock._break(); + + result = JExpr.ref(responseEntity.name()); + usesInstance = true; + } else { + result = restCall; + usesInstance = false; + } + + insertRestCallInBody(body, result, methodHolder, methodReturnVoid, usesInstance); } /** @@ -132,20 +198,20 @@ protected JInvocation addResponseEntityArg(JInvocation restCall, MethodProcessor * Add an extra method calls on the result of restTemplate.exchange(). By * default, just return the result */ - protected JInvocation addResultCallMethod(JInvocation restCall, MethodProcessorHolder methodHolder) { + protected JExpression addResultCallMethod(JExpression restCall, MethodProcessorHolder methodHolder) { return restCall; } - private void insertRestCallInBody(JBlock body, JInvocation restCall, MethodProcessorHolder methodHolder, boolean methodReturnVoid) { - if (methodReturnVoid) { - body.add(restCall); - } else { + private void insertRestCallInBody(JBlock body, JExpression restCall, MethodProcessorHolder methodHolder, boolean methodReturnVoid, boolean usesInstance) { + if (methodReturnVoid && !usesInstance && restCall instanceof JInvocation) { + body.add((JInvocation) restCall); + } else if (!methodReturnVoid) { restCall = addResultCallMethod(restCall, methodHolder); body._return(restCall); } } - private JVar generateHashMapVar(MethodProcessorHolder methodHolder) { + private JVar generateHashMapVar(RestImplementationHolder holder, MethodProcessorHolder methodHolder) { ExecutableElement element = (ExecutableElement) methodHolder.getElement(); JCodeModel codeModel = methodHolder.getCodeModel(); JBlock body = methodHolder.getBody(); @@ -153,14 +219,29 @@ private JVar generateHashMapVar(MethodProcessorHolder methodHolder) { JVar hashMapVar = null; Set urlVariables = restAnnotationHelper.extractUrlVariableNames(element); + + // cookies in url? + String[] cookiesToUrl = retrieveRequiredUrlCookieNames(element); + if (cookiesToUrl != null) { + for (String cookie : cookiesToUrl) { + urlVariables.add(cookie); + } + } + JClass hashMapClass = codeModel.ref(HashMap.class).narrow(String.class, Object.class); if (!urlVariables.isEmpty()) { hashMapVar = body.decl(hashMapClass, "urlVariables", JExpr._new(hashMapClass)); for (String urlVariable : urlVariables) { - JVar urlValue = methodParams.get(urlVariable); - body.invoke(hashMapVar, "put").arg(urlVariable).arg(urlValue); - methodParams.remove(urlVariable); + JVar methodParam = methodParams.get(urlVariable); + if (methodParam != null) { + body.invoke(hashMapVar, "put").arg(urlVariable).arg(methodParam); + methodParams.remove(urlVariable); + } else { + // cookie from url + JInvocation cookieValue = holder.availableCookiesField.invoke("get").arg(JExpr.lit(urlVariable)); + body.invoke(hashMapVar, "put").arg(urlVariable).arg(cookieValue); + } } } return hashMapVar; @@ -191,7 +272,7 @@ protected JExpression generateHttpEntityVar(MethodProcessorHolder methodHolder) } JBlock body = methodHolder.getBody(); - JVar httpHeadersVar = generateHttpHeadersVar(holder, body, executableElement); + JVar httpHeadersVar = generateHttpHeadersVar(methodHolder, holder, body, executableElement); boolean hasHeaders = httpHeadersVar != null; @@ -216,16 +297,28 @@ protected JExpression generateHttpEntityVar(MethodProcessorHolder methodHolder) return httpEntityVar; } - protected JVar generateHttpHeadersVar(EBeanHolder holder, JBlock body, ExecutableElement executableElement) { + protected JVar generateHttpHeadersVar(MethodProcessorHolder methodHolder, EBeanHolder holder, JBlock body, ExecutableElement executableElement) { JVar httpHeadersVar = null; JClass httpHeadersClass = holder.refClass(CanonicalNameConstants.HTTP_HEADERS); String mediaType = retrieveAcceptAnnotationValue(executableElement); boolean hasMediaTypeDefined = mediaType != null; - if (hasMediaTypeDefined) { + + String cookies[] = retrieveRequiredCookieNames(executableElement); + boolean requiresCookies = cookies != null && cookies.length > 0; + + String headers[] = retrieveRequiredHeaderNames(executableElement); + boolean requiresHeaders = headers != null && headers.length > 0; + + boolean requiresAuth = requiresAuth(executableElement); + + if (hasMediaTypeDefined || requiresCookies || requiresHeaders || requiresAuth) { + // we need the headers httpHeadersVar = body.decl(httpHeadersClass, "httpHeaders", JExpr._new(httpHeadersClass)); + } + if (hasMediaTypeDefined) { JClass collectionsClass = holder.refClass(CanonicalNameConstants.COLLECTIONS); JClass mediaTypeClass = holder.refClass(CanonicalNameConstants.MEDIA_TYPE); @@ -233,6 +326,38 @@ protected JVar generateHttpHeadersVar(EBeanHolder holder, JBlock body, Executabl body.add(JExpr.invoke(httpHeadersVar, "setAccept").arg(mediaTypeListParam)); } + if (requiresCookies) { + RestImplementationHolder restHolder = restImplementationsHolder.getEnclosingHolder(methodHolder.getElement()); + + JClass stringClass = holder.refClass(CanonicalNameConstants.STRING); + JClass stringBuilderClass = holder.refClass("java.lang.StringBuilder"); + JVar cookiesValueVar = body.decl(stringBuilderClass, "cookiesValue", JExpr._new(stringBuilderClass)); + for (String cookie : cookies) { + JInvocation cookieValue = JExpr.invoke(restHolder.availableCookiesField, "get").arg(cookie); + JInvocation cookieFormatted = stringClass.staticInvoke("format").arg(String.format("%s=%%s;", cookie)).arg(cookieValue); + JInvocation appendCookie = JExpr.invoke(cookiesValueVar, "append").arg(cookieFormatted); + body.add(appendCookie); + } + + JInvocation cookiesToString = cookiesValueVar.invoke("toString"); + body.add(JExpr.invoke(httpHeadersVar, "set").arg("Cookie").arg(cookiesToString)); + } + + if (requiresHeaders) { + RestImplementationHolder restHolder = restImplementationsHolder.getEnclosingHolder(methodHolder.getElement()); + for (String header : headers) { + JInvocation headerValue = JExpr.invoke(restHolder.availableHeadersField, "get").arg(header); + body.add(JExpr.invoke(httpHeadersVar, "set").arg(header).arg(headerValue)); + } + + } + + if (requiresAuth) { + // attach auth + RestImplementationHolder restHolder = restImplementationsHolder.getEnclosingHolder(methodHolder.getElement()); + body.add(httpHeadersVar.invoke("setAuthorization").arg(restHolder.authenticationField)); + } + return httpHeadersVar; } @@ -248,6 +373,62 @@ private String retrieveAcceptAnnotationValue(ExecutableElement executableElement } } + private String[] retrieveRequiredHeaderNames(ExecutableElement executableElement) { + RequiresHeader cookieAnnotation = executableElement.getAnnotation(RequiresHeader.class); + if (cookieAnnotation == null) { + cookieAnnotation = executableElement.getEnclosingElement().getAnnotation(RequiresHeader.class); + } + if (cookieAnnotation != null) { + return cookieAnnotation.value(); + } else { + return null; + } + } + + private String[] retrieveRequiredCookieNames(ExecutableElement executableElement) { + RequiresCookie cookieAnnotation = executableElement.getAnnotation(RequiresCookie.class); + if (cookieAnnotation == null) { + cookieAnnotation = executableElement.getEnclosingElement().getAnnotation(RequiresCookie.class); + } + if (cookieAnnotation != null) { + return cookieAnnotation.value(); + } else { + return null; + } + } + + public static String[] retrieveRequiredUrlCookieNames(ExecutableElement executableElement) { + RequiresCookieInUrl cookieAnnotation = executableElement.getAnnotation(RequiresCookieInUrl.class); + if (cookieAnnotation == null) { + cookieAnnotation = executableElement.getEnclosingElement().getAnnotation(RequiresCookieInUrl.class); + } + if (cookieAnnotation != null) { + return cookieAnnotation.value(); + } else { + return null; + } + } + + private String[] retrieveSettingCookieNames(ExecutableElement executableElement) { + SetsCookie cookieAnnotation = executableElement.getAnnotation(SetsCookie.class); + if (cookieAnnotation == null) { + cookieAnnotation = executableElement.getEnclosingElement().getAnnotation(SetsCookie.class); + } + if (cookieAnnotation != null) { + return cookieAnnotation.value(); + } else { + return null; + } + } + + private boolean requiresAuth(ExecutableElement executableElement) { + RequiresAuthentication basicAuthAnnotation = executableElement.getAnnotation(RequiresAuthentication.class); + if (basicAuthAnnotation == null) { + basicAuthAnnotation = executableElement.getEnclosingElement().getAnnotation(RequiresAuthentication.class); + } + return basicAuthAnnotation != null; + } + private TreeMap extractMethodParamsVar(EBeanHolder eBeanHolder, JMethod method, ExecutableElement executableElement, RestImplementationHolder holder) { List params = executableElement.getParameters(); TreeMap methodParams = new TreeMap(); diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/OptionsProcessor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/OptionsProcessor.java index e2f93a1dfe..3f46b72b07 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/OptionsProcessor.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/OptionsProcessor.java @@ -28,7 +28,7 @@ import com.sun.codemodel.JClass; import com.sun.codemodel.JCodeModel; import com.sun.codemodel.JExpr; -import com.sun.codemodel.JInvocation; +import com.sun.codemodel.JExpression; public class OptionsProcessor extends MethodProcessor { @@ -63,7 +63,7 @@ public void process(Element element, JCodeModel codeModel, EBeanHolder holder) t } @Override - protected JInvocation addResultCallMethod(JInvocation restCall, MethodProcessorHolder methodHolder) { + protected JExpression addResultCallMethod(JExpression restCall, MethodProcessorHolder methodHolder) { restCall = JExpr.invoke(restCall, "getHeaders"); restCall = JExpr.invoke(restCall, "getAllow"); return restCall; diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/RestImplementationHolder.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/RestImplementationHolder.java index 7ced7511a8..58995390e8 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/RestImplementationHolder.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/RestImplementationHolder.java @@ -26,4 +26,10 @@ public class RestImplementationHolder { public JFieldVar rootUrlField; + public JFieldVar availableHeadersField; + + public JFieldVar availableCookiesField; + + public JFieldVar authenticationField; + } diff --git a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/RestProcessor.java b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/RestProcessor.java index b0f49bcc1b..5feb48ae6c 100644 --- a/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/RestProcessor.java +++ b/AndroidAnnotations/androidannotations/src/main/java/org/androidannotations/processing/rest/RestProcessor.java @@ -45,6 +45,9 @@ import com.sun.codemodel.JBlock; import com.sun.codemodel.JClass; import com.sun.codemodel.JCodeModel; +import com.sun.codemodel.JExpr; +import com.sun.codemodel.JFieldVar; +import com.sun.codemodel.JInvocation; import com.sun.codemodel.JMethod; import com.sun.codemodel.JMod; import com.sun.codemodel.JVar; @@ -88,6 +91,15 @@ public void process(Element element, JCodeModel codeModel, EBeansHolder eBeansHo JClass stringClass = eBeansHolder.refClass(STRING); holder.rootUrlField = holder.restImplementationClass.field(JMod.PRIVATE, stringClass, "rootUrl"); + // available headers/cookies + JClass mapClass = eBeansHolder.refClass("java.util.HashMap").narrow(stringClass, stringClass); + holder.availableHeadersField = holder.restImplementationClass.field(JMod.PRIVATE, mapClass, "availableHeaders"); + holder.availableCookiesField = holder.restImplementationClass.field(JMod.PRIVATE, mapClass, "availableCookies"); + + // any auth + JClass httpAuthClass = eBeansHolder.refClass("org.springframework.http.HttpAuthentication"); + holder.authenticationField = holder.restImplementationClass.field(JMod.PRIVATE, httpAuthClass, "authentication"); + { // Constructor JMethod constructor = holder.restImplementationClass.constructor(JMod.PUBLIC); @@ -118,6 +130,9 @@ public void process(Element element, JCodeModel codeModel, EBeansHolder eBeansHo } } constructorBody.assign(holder.rootUrlField, lit(typeElement.getAnnotation(Rest.class).rootUrl())); + + constructorBody.assign(holder.availableHeadersField, _new(mapClass)); + constructorBody.assign(holder.availableCookiesField, _new(mapClass)); } // Implement getRestTemplate method @@ -170,6 +185,91 @@ public void process(Element element, JCodeModel codeModel, EBeansHolder eBeansHo } } + // Implement setHttpBasicAuth method + for (ExecutableElement method : methods) { + List parameters = method.getParameters(); + if (parameters.size() == 2 && method.getReturnType().getKind() == TypeKind.VOID) { + VariableElement firstParameter = parameters.get(0); + VariableElement secondParameter = parameters.get(1); + if (firstParameter.asType().toString().equals(STRING) && secondParameter.asType().toString().equals(STRING) && method.getSimpleName().toString().equals("setHttpBasicAuth")) { + JMethod setBasicAuthMethod = holder.restImplementationClass.method(JMod.PUBLIC, codeModel.VOID, method.getSimpleName().toString()); + setBasicAuthMethod.annotate(Override.class); + + JVar userParam = setBasicAuthMethod.param(stringClass, firstParameter.getSimpleName().toString()); + JVar passParam = setBasicAuthMethod.param(stringClass, secondParameter.getSimpleName().toString()); + + JClass basicAuthClass = eBeansHolder.refClass("org.springframework.http.HttpBasicAuthentication"); + JInvocation basicAuthentication = JExpr._new(basicAuthClass).arg(userParam).arg(passParam); + + setBasicAuthMethod.body().assign(_this().ref(holder.authenticationField), basicAuthentication); + break; // Only one implementation + } + } + } + + // Implement setAuthentication method + for (ExecutableElement method : methods) { + List parameters = method.getParameters(); + if (parameters.size() == 1 && method.getReturnType().getKind() == TypeKind.VOID) { + VariableElement firstParameter = parameters.get(0); + if (firstParameter.asType().toString().equals("org.springframework.http.HttpAuthentication") && method.getSimpleName().toString().equals("setAuthentication")) { + JMethod setAuthMethod = holder.restImplementationClass.method(JMod.PUBLIC, codeModel.VOID, method.getSimpleName().toString()); + setAuthMethod.annotate(Override.class); + + JClass authClass = eBeansHolder.refClass("org.springframework.http.HttpAuthentication"); + JVar authParam = setAuthMethod.param(authClass, firstParameter.getSimpleName().toString()); + + setAuthMethod.body().assign(_this().ref(holder.authenticationField), authParam); + break; // Only one implementation + } + } + } + + // Implement getCookie and getHeader methods + implementMapGetMethod(holder, stringClass, methods, holder.availableCookiesField, "getCookie"); + implementMapGetMethod(holder, stringClass, methods, holder.availableHeadersField, "getHeader"); + + // Implement putCookie and putHeader methods + implementMapPutMethod(holder, stringClass, codeModel, methods, holder.availableCookiesField, "setCookie"); + implementMapPutMethod(holder, stringClass, codeModel, methods, holder.availableHeadersField, "setHeader"); + } + + private void implementMapGetMethod(RestImplementationHolder holder, JClass stringClass, List methods, JFieldVar field, String methodName) { + for (ExecutableElement method : methods) { + List parameters = method.getParameters(); + if (parameters.size() == 1 && method.getReturnType().toString().equals(STRING)) { + VariableElement firstParameter = parameters.get(0); + if (firstParameter.asType().toString().equals(STRING) && method.getSimpleName().toString().equals(methodName)) { + JMethod getCookieMethod = holder.restImplementationClass.method(JMod.PUBLIC, stringClass, method.getSimpleName().toString()); + getCookieMethod.annotate(Override.class); + + JVar cookieNameParam = getCookieMethod.param(stringClass, firstParameter.getSimpleName().toString()); + JInvocation cookieValue = JExpr.invoke(field, "get").arg(cookieNameParam); + getCookieMethod.body()._return(cookieValue); + break; // Only one implementation + } + } + } + } + + private void implementMapPutMethod(RestImplementationHolder holder, JClass stringClass, JCodeModel codeModel, List methods, JFieldVar field, String methodName) { + for (ExecutableElement method : methods) { + List parameters = method.getParameters(); + if (parameters.size() == 2 && method.getReturnType().getKind() == TypeKind.VOID) { + VariableElement firstParameter = parameters.get(0); + VariableElement secondParameter = parameters.get(1); + if (firstParameter.asType().toString().equals(STRING) && secondParameter.asType().toString().equals(STRING) && method.getSimpleName().toString().equals(methodName)) { + JMethod putMapMethod = holder.restImplementationClass.method(JMod.PUBLIC, codeModel.VOID, method.getSimpleName().toString()); + putMapMethod.annotate(Override.class); + + JVar keyParam = putMapMethod.param(stringClass, firstParameter.getSimpleName().toString()); + JVar valParam = putMapMethod.param(stringClass, secondParameter.getSimpleName().toString()); + + putMapMethod.body().invoke(field, "put").arg(keyParam).arg(valParam); + break; // Only one implementation + } + } + } } private void implementGetRootUrl(RestImplementationHolder holder, EBeansHolder eBeansHolder, List methods) { diff --git a/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/rest/MyServiceTest.java b/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/rest/MyServiceTest.java index 477e2d8679..28e8340933 100644 --- a/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/rest/MyServiceTest.java +++ b/AndroidAnnotations/functional-test-1-5-tests/src/test/java/org/androidannotations/test15/rest/MyServiceTest.java @@ -16,24 +16,31 @@ package org.androidannotations.test15.rest; import static junit.framework.Assert.assertEquals; +import static org.mockito.Matchers.argThat; +import static org.mockito.Matchers.eq; import static org.mockito.Matchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.androidannotations.test15.AndroidAnnotationsTestRunner; +import org.apache.http.Header; import org.apache.http.message.BasicHeader; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; import org.mockito.Mockito; import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; +import com.google.inject.matcher.Matchers; import com.xtremelabs.robolectric.Robolectric; @RunWith(AndroidAnnotationsTestRunner.class) @@ -42,9 +49,18 @@ public class MyServiceTest { private MyService_ myService = new MyService_(); private void addPendingResponse(String jsonResponse) { - Robolectric.addPendingHttpResponse(HttpStatus.OK.value(), jsonResponse.replaceAll("'", "\""), new BasicHeader("content-type", "application/json")); + addPendingResponse(jsonResponse, "_=_"); + } + private void addPendingResponse(String jsonResponse, String... cookies) { + Header[] headers = new Header[1 + cookies.length/2]; + headers[0] = new BasicHeader("content-type", "application/json"); + for (int i=0, j=1; i < cookies.length-1; i += 2, j++) { + headers[j] = new BasicHeader("set-cookie", cookies[i] + "=" + cookies[i+1]); + } + Robolectric.addPendingHttpResponse(HttpStatus.OK.value(), jsonResponse.replaceAll("'", "\""), headers); } + @Test public void can_override_root_url() { MyService_ myService = new MyService_(); @@ -162,5 +178,77 @@ public void urlWithAParameterDeclaredTwiceTest() { } } } + + @Test + public void manualFullUrl() { + + MyService_ myService = new MyService_(); + + RestTemplate restTemplate = mock(RestTemplate.class); + myService.setRestTemplate(restTemplate); + + // make sure we used the full custom url. + // this may be used like in Google's APIs + // to fetch an oauth token; Mockito doesn't + // return a response with the mock'd template, + // so we just use this weird "ping" endpoint + addPendingResponse("fancyHeaderToken"); + myService.setHttpBasicAuth("fancyUser", "fancierPassword"); + myService.ping(); + verify(restTemplate).exchange(eq("http://company.com/client/ping"), Mockito. any(), Mockito.> any(), Mockito.> any()); + } + + @Test + public void cookieInUrl() { + + final String xtValue = "1234"; + final String sjsaidValue = "7890"; + final String locationValue = "somePlace"; + final int yearValue = 2013; + + MyService_ myService = new MyService_(); + + RestTemplate restTemplate = mock(RestTemplate.class); + myService.setRestTemplate(restTemplate); + + addPendingResponse("{'id':1,'name':'event1'}"); + + // normally this is set by a call like authenticate() + // which is annotated with @SetsCookie + myService.setCookie("xt", xtValue); + myService.setCookie("sjsaid", sjsaidValue); + myService.setHttpBasicAuth("fancyUser", "fancierPassword"); + myService.getEventsVoid(locationValue, yearValue); + + ArgumentMatcher> matcher = new ArgumentMatcher>() { + + @Override + public boolean matches(Object argument) { + final String expected = "sjsaid=" + sjsaidValue + ";"; + return expected.equals(((HttpEntity) argument).getHeaders().get("Cookie").get(0)); + } + }; + + Map urlVariables = new HashMap(); + urlVariables.put("location", locationValue); + urlVariables.put("year", yearValue); + urlVariables.put("xt", xtValue); + verify(restTemplate).exchange(Mockito.anyString(), Mockito. any(), argThat(matcher), Mockito.> any(), eq(urlVariables)); + } + + @Test + public void authenticatedRequests() { + + String xtValue = "1234"; + String sjsaidValue = "5678"; + myService.setHeader("SomeFancyHeader", "fancyHeaderToken"); + + addPendingResponse("[]", "xt", xtValue, "sjsaid", sjsaidValue); + myService.authenticate(); + + assertEquals(xtValue, myService.getCookie("xt")); + assertEquals(sjsaidValue, myService.getCookie("sjsaid")); + + } } diff --git a/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/ParcelableSerializableData.java b/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/ParcelableSerializableData.java index 747b8ed9b5..d5ae931ff1 100644 --- a/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/ParcelableSerializableData.java +++ b/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/ParcelableSerializableData.java @@ -1,3 +1,18 @@ +/** + * Copyright (C) 2010-2013 eBusiness Information, Excilys Group + * + * Licensed 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.androidannotations.test15; import java.io.Serializable; diff --git a/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/rest/MyService.java b/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/rest/MyService.java index a6eb34917b..caa460171d 100644 --- a/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/rest/MyService.java +++ b/AndroidAnnotations/functional-test-1-5/src/main/java/org/androidannotations/test15/rest/MyService.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Set; +import org.springframework.http.HttpAuthentication; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; @@ -33,7 +34,12 @@ import org.androidannotations.annotations.rest.Options; import org.androidannotations.annotations.rest.Post; import org.androidannotations.annotations.rest.Put; +import org.androidannotations.annotations.rest.RequiresAuthentication; +import org.androidannotations.annotations.rest.RequiresCookie; +import org.androidannotations.annotations.rest.RequiresCookieInUrl; +import org.androidannotations.annotations.rest.RequiresHeader; import org.androidannotations.annotations.rest.Rest; +import org.androidannotations.annotations.rest.SetsCookie; import org.androidannotations.api.rest.MediaType; // if defined, the rootUrl will be added as a prefix to every request @@ -43,6 +49,7 @@ public interface MyService { // *** GET *** // url variables are mapped to method parameter names. + @RequiresCookie("xt") @Get("/events/{year}/{location}") @Accept(MediaType.APPLICATION_JSON) EventList getEvents(String location, int year); @@ -112,10 +119,20 @@ ResponseEntity getEventsArrayOfArrays2(String location, int year) @Get("/events/{year}/{location}") Map getEventsGenericsMap(String location, int year) throws RestClientException; - @Get("/events/{year}/{location}") + @RequiresCookie("sjsaid") + @RequiresCookieInUrl("xt") + @Get("/events/{year}/{location}?xt={xt}") void getEventsVoid(String location, int year) throws RestClientException; // *** POST *** + @RequiresHeader("SomeFancyHeader") + @Post("/login") + @SetsCookie({"xt", "sjsaid"}) + void authenticate(); + + @RequiresAuthentication + @Post("http://company.com/client/ping") + void ping(); // There should be max 1 parameter that is not mapped to an attribute. This // parameter will be used as the post entity. @@ -208,4 +225,16 @@ ResponseEntity getEventsArrayOfArrays2(String location, int year) void setRootUrl(String test); String getRootUrl(); + + void setCookie(String cookieName, String value); + + String getCookie(String cookieName); + + void setHeader(String headerName, String value); + + String getHeader(String headerName); + + void setAuthentication(HttpAuthentication auth); + + void setHttpBasicAuth(String username, String password); }