├── .gitignore ├── README.md ├── build.gradle ├── clients ├── client-android-apache-client │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── techery │ │ └── janet │ │ └── android │ │ ├── AndroidApacheClient.java │ │ └── ApacheClient.java ├── client-okhttp │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── techery │ │ └── janet │ │ └── okhttp │ │ └── OkClient.java ├── client-okhttp3 │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── techery │ │ └── janet │ │ └── okhttp3 │ │ └── OkClient.java └── client-url-connection │ ├── build.gradle │ └── src │ └── main │ └── java │ └── io │ └── techery │ └── janet │ └── http │ └── UrlConnectionClient.java ├── gradle ├── plugins │ └── maven-simple.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── build.gradle └── src │ └── main │ ├── java │ └── io │ │ └── techery │ │ └── janet │ │ └── http │ │ └── sample │ │ ├── HttpSample.java │ │ ├── action │ │ ├── TestProgressAction.java │ │ ├── UserReposAction.java │ │ ├── UsersAction.kt │ │ └── base │ │ │ ├── BaseAction.java │ │ │ └── ServerAction.java │ │ ├── model │ │ ├── Repository.java │ │ └── User.java │ │ └── util │ │ └── SampleLoggingService.java │ └── kotlin │ └── io │ └── techery │ └── janet │ └── http │ └── sample │ └── KSimpleService.kt ├── service-compiler ├── build.gradle └── src │ └── main │ └── java │ └── io │ └── techery │ └── janet │ ├── HttpActionClass.java │ ├── HttpHelpersFactoryGenerator.java │ ├── HttpHelpersGenerator.java │ ├── JanetHttpProcessor.java │ ├── Options.java │ ├── util │ └── ElementResolver.java │ └── validation │ ├── AnnotationQuantityValidator.java │ ├── AnnotationTypesValidator.java │ ├── BodyValidator.java │ ├── FieldsModifiersValidator.java │ ├── HttpActionValidators.java │ ├── ParentsValidator.java │ ├── PathValidator.java │ ├── RequestTypeValidator.java │ ├── ResponseValidator.java │ └── UrlValidator.java ├── service ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ └── java │ └── io │ └── techery │ └── janet │ ├── HttpActionService.java │ ├── RequestBuilder.java │ └── http │ ├── HttpClient.java │ ├── annotations │ ├── Body.java │ ├── Field.java │ ├── HttpAction.java │ ├── Part.java │ ├── Path.java │ ├── Query.java │ ├── RequestHeader.java │ ├── Response.java │ ├── ResponseHeader.java │ ├── Status.java │ └── Url.java │ ├── exception │ ├── HttpDeserializationException.java │ ├── HttpException.java │ ├── HttpSerializationException.java │ └── HttpServiceException.java │ ├── internal │ └── ProgressOutputStream.java │ ├── model │ ├── FormUrlEncodedRequestBody.java │ ├── Header.java │ ├── MimeOverridingTypedOutput.java │ ├── MultipartRequestBody.java │ ├── Request.java │ └── Response.java │ ├── test │ └── MockHttpActionService.java │ └── utils │ └── RequestUtils.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | # Archives 29 | *.gz 30 | *.tar 31 | *.zip 32 | 33 | # Eclipse 34 | *.metadata 35 | *.settings 36 | 37 | # Android Studio 38 | .idea/* 39 | **/*/.idea 40 | !.idea/codeStyleSettings.xml 41 | !.idea/codeStyles 42 | .gradle 43 | */local.properties 44 | */out 45 | *.iml 46 | *.iws 47 | *.ipr 48 | *~ 49 | *.swp 50 | 51 | # OS specific 52 | .DS_Store 53 | 54 | # svn 55 | *.svn* 56 | 57 | #node.js 58 | node_modules 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Http ActionService 2 | REST service for [Janet](https://github.com/techery/janet). Supports fully customizable requests, http-clients and converters. 3 | 4 | ### Getting Started 5 | ##### 1. Define service and add it to `Janet` 6 | ```java 7 | ActionService httpService = new HttpActionService(API_URL, new OkClient(), new GsonConverter(new Gson())) 8 | Janet janet = new Janet.Builder().addService(httpService).build(); 9 | ``` 10 | 11 | Service requires: End-point url, [HttpClient](clients) and [Converter](https://github.com/techery/janet-converters). 12 | 13 | ##### 2. Define Request-Response action class 14 | ```java 15 | @HttpAction(value = "/demo", method = HttpAction.Method.GET) 16 | public class ExampleAction { 17 | @Response ExampleDataModel responseData; 18 | } 19 | ``` 20 | Each action is an individual class that contains all information about the request and response. 21 | It must be annotated with `@HttpAction` 22 | 23 | ##### 3. Use `ActionPipe` to send/observe action 24 | ```java 25 | ActionPipe actionPipe = janet.createPipe(ExampleAction.class); 26 | actionPipe 27 | .createObservable(new ExampleAction()) 28 | .subscribeOn(Schedulers.io()) 29 | .subscribe(new ActionStateSubscriber() 30 | .onProgress((action, progress) -> System.out.println("Current progress: " + progress)) 31 | .onSuccess(action -> System.out.println("Got example " + action)) 32 | .onFail((action, throwable) -> System.err.println("Bad things happened " + throwable)) 33 | ); 34 | ``` 35 | 36 | ### Http Action Configuration 37 | 38 | Request path, method and type are defined by `@HttpAction` annotation: 39 | * `value` – url path 40 | * `method` – get/post/put/delete/head/patch 41 | * `type` –  simple/multipart/form_url_encoded 42 | 43 | To configure request, Action fields can be annotated with: 44 | * `@Path` for path value 45 | * `@Url` rewrites full url or suffixes 46 | * `@Query` for request URL parameters 47 | * `@Body` for POST request body 48 | * `@Field` for request fields if request type is `HttpAction.Type.FORM_URL_ENCODED` 49 | * `@Part` for multipart request parts 50 | * `@RequestHeader` for request headers 51 | 52 | To process response, special annotations can be used: 53 | * `@Response` for getting response body. 54 | * `@Status` for getting response status. Field types `Integer`, `Long`, `int` or `long` can be used to get status code or use `boolean` to know that request was sent successfully 55 | * `@ResponseHeader` for getting response headers 56 | 57 | Example: 58 | ```java 59 | @HttpAction( 60 | value = "/demo/{examplePath}/info", 61 | method = HttpAction.Method.GET, 62 | type = HttpAction.Type.SIMPLE 63 | ) 64 | public class ExampleAction { 65 | // Request params 66 | @Path("examplePath") String pathValue; 67 | @Query("someParam") int queryParam; 68 | @RequestHeader("Example-RequestHeader-Name") String requestHeaderValue; 69 | // Response data 70 | @Status int statusCode; 71 | @Response ExampleDataModel exampleDataModel; 72 | @ResponseHeader("Example-ResponseHeader-Name") String responseHeaderValue; 73 | } 74 | ``` 75 | 76 | ### Advanced bits 77 | * supports request progress; 78 | * supports request cancelation; 79 | * provides useful `HttpException` for failed requests; 80 | * supports action inheritance 81 | * based on annotation processing 82 | * consider using javac option `'-Ajanet.http.factory.class.suffix=MyLib'` for api libraries 83 | 84 | ### Kotlin support 85 | Kotlin action classes are supported except internal modifier. See [UsersAction](sample/src/main/java/io/techery/janet/http/sample/action/UsersAction.kt) as example. 86 | 87 | ### Download 88 | ```groovy 89 | repositories { 90 | jcenter() 91 | maven { url "https://jitpack.io" } 92 | } 93 | 94 | dependencies { 95 | compile 'com.github.techery.janet-http:service:xxx' 96 | apt 'com.github.techery.janet-http:service-compiler:xxx' 97 | compile 'com.github.techery.janet-http:client-okhttp:xxx' 98 | compile 'com.github.techery.janet-converters:gson:yyy' 99 | // it is recommended you also explicitly depend on latest Janet version for bug fixes and new features. 100 | compile 'com.github.techery:janet:zzz' 101 | } 102 | ``` 103 | * janet: [![](https://jitpack.io/v/techery/janet.svg)](https://jitpack.io/#techery/janet) 104 | * janet-http: [![](https://jitpack.io/v/techery/janet-http.svg)](https://jitpack.io/#techery/janet-http) 105 | * janet-converters: [![](https://jitpack.io/v/techery/janet-converters.svg)](https://jitpack.io/#techery/janet-converters) 106 | 107 | ### Recipes 108 | * Authorize requests via `ActionServiceWrapper`, e.g. [AuthWrapper](https://github.com/techery/janet-architecture-sample/blob/eff90f2f0a0013648263631a40bf3e76f7b9dfa2/app/src/main/java/io/techery/sample/service/AuthServiceWrapper.java) 109 | * Log requests via `HttpClient` or `ActionServiceWrapper`, e.g. [SampleLoggingService](sample/src/main/java/io/techery/janet/http/sample/util/SampleLoggingService.java) 110 | * Convert `Retrofit` interfaces into actions with [Converter Util](https://github.com/techery/janet-retrofit-converter) 111 | * Write tests using `MockHttpActionService` 112 | * See more samples: 113 | [Simple Android app](https://github.com/techery/janet-http-android-sample), 114 | [Advanced Android app](https://github.com/techery/janet-architecture-sample) 115 | 116 | ### Proguard 117 | * Add [Rules](service/proguard-rules.pro) to your proguard config. 118 | * See [Android Sample](https://github.com/techery/janet-http-android-sample) for complete proguard config example. 119 | 120 | ### Notes 121 | `HttpActionService` is highly inspired by `Retrofit2` – thanks guys, it's awesome! 122 | We put our effort to make it even more flexible and reusable, so everyone who loves `Retrofit` and reactive approach should give it a try. 123 | 124 | ## License 125 | 126 | Copyright (c) 2016 Techery 127 | 128 | Licensed under the Apache License, Version 2.0 (the "License"); 129 | you may not use this file except in compliance with the License. 130 | You may obtain a copy of the License at 131 | 132 | http://www.apache.org/licenses/LICENSE-2.0 133 | 134 | Unless required by applicable law or agreed to in writing, software 135 | distributed under the License is distributed on an "AS IS" BASIS, 136 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 137 | See the License for the specific language governing permissions and 138 | limitations under the License. 139 | 140 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | jcenter() 4 | maven { url "https://jitpack.io" } 5 | } 6 | } -------------------------------------------------------------------------------- /clients/client-android-apache-client/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.netflix.nebula:gradle-extra-configurations-plugin:3.0.3' 7 | } 8 | } 9 | 10 | apply plugin: 'java-library' 11 | apply plugin: 'nebula.provided-base' 12 | apply from: rootProject.file('gradle/plugins/maven-simple.gradle') 13 | 14 | compileJava { 15 | sourceCompatibility = '1.6' 16 | targetCompatibility = '1.6' 17 | } 18 | 19 | dependencies { 20 | compile project(':service') 21 | provided 'com.google.android:android:4.1.1.4' 22 | } 23 | -------------------------------------------------------------------------------- /clients/client-android-apache-client/src/main/java/io/techery/janet/android/AndroidApacheClient.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.android; 2 | 3 | import android.net.http.AndroidHttpClient; 4 | 5 | public final class AndroidApacheClient extends ApacheClient { 6 | public AndroidApacheClient() { 7 | super(AndroidHttpClient.newInstance("Janet")); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /clients/client-android-apache-client/src/main/java/io/techery/janet/android/ApacheClient.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.android; 2 | 3 | import org.apache.http.HttpEntity; 4 | import org.apache.http.HttpResponse; 5 | import org.apache.http.StatusLine; 6 | import org.apache.http.client.HttpClient; 7 | import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 8 | import org.apache.http.client.methods.HttpRequestBase; 9 | import org.apache.http.client.methods.HttpUriRequest; 10 | import org.apache.http.entity.AbstractHttpEntity; 11 | import org.apache.http.impl.client.DefaultHttpClient; 12 | import org.apache.http.message.BasicHeader; 13 | import org.apache.http.params.BasicHttpParams; 14 | import org.apache.http.params.HttpConnectionParams; 15 | import org.apache.http.params.HttpParams; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | import java.net.URI; 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | 24 | import io.techery.janet.body.ActionBody; 25 | import io.techery.janet.http.internal.ProgressOutputStream; 26 | import io.techery.janet.http.model.Header; 27 | import io.techery.janet.http.model.Request; 28 | import io.techery.janet.http.model.Response; 29 | import io.techery.janet.http.utils.RequestUtils; 30 | 31 | 32 | public class ApacheClient implements io.techery.janet.http.HttpClient { 33 | 34 | private static HttpClient createDefaultClient() { 35 | HttpParams params = new BasicHttpParams(); 36 | HttpConnectionParams.setConnectionTimeout(params, CONNECT_TIMEOUT_MILLIS); 37 | HttpConnectionParams.setSoTimeout(params, READ_TIMEOUT_MILLIS); 38 | return new DefaultHttpClient(params); 39 | } 40 | 41 | private final HttpClient client; 42 | 43 | public ApacheClient() { 44 | this(createDefaultClient()); 45 | } 46 | 47 | public ApacheClient(HttpClient client) { 48 | this.client = client; 49 | } 50 | 51 | @Override public Response execute(Request request, RequestCallback requestCallback) throws IOException { 52 | HttpUriRequest apacheRequest = createRequest(request, requestCallback); 53 | RequestUtils.throwIfCanceled(request); 54 | request.tag = apacheRequest; //mark for cancellation 55 | HttpResponse apacheResponse = execute(client, apacheRequest); 56 | RequestUtils.throwIfCanceled(request); 57 | return parseResponse(request.getUrl(), apacheResponse); 58 | } 59 | 60 | @Override public void cancel(Request request) { 61 | if (request.tag != null && (request.tag instanceof HttpUriRequest)) { 62 | HttpUriRequest apacheRequest = (HttpUriRequest) request.tag; 63 | apacheRequest.abort(); 64 | } 65 | request.tag = RequestUtils.TAG_CANCELED; 66 | } 67 | 68 | protected HttpResponse execute(HttpClient client, HttpUriRequest request) throws IOException { 69 | return client.execute(request); 70 | } 71 | 72 | static HttpUriRequest createRequest(Request request, RequestCallback requestCallback) { 73 | if (request.getBody() != null) { 74 | return new GenericEntityHttpRequest(request, requestCallback); 75 | } 76 | return new GenericHttpRequest(request); 77 | } 78 | 79 | static Response parseResponse(String url, HttpResponse response) throws IOException { 80 | StatusLine statusLine = response.getStatusLine(); 81 | int status = statusLine.getStatusCode(); 82 | String reason = statusLine.getReasonPhrase(); 83 | 84 | List
headers = new ArrayList
(); 85 | String contentType = "application/octet-stream"; 86 | for (org.apache.http.Header header : response.getAllHeaders()) { 87 | String name = header.getName(); 88 | String value = header.getValue(); 89 | if ("Content-Type".equalsIgnoreCase(name)) { 90 | contentType = value; 91 | } 92 | headers.add(new Header(name, value)); 93 | } 94 | 95 | ActionBody body = null; 96 | HttpEntity entity = response.getEntity(); 97 | if (entity != null) { 98 | body = new ResponseActionBody(entity); 99 | } 100 | 101 | return new Response(url, status, reason, headers, body); 102 | } 103 | 104 | private static class GenericHttpRequest extends HttpRequestBase { 105 | private final String method; 106 | 107 | public GenericHttpRequest(Request request) { 108 | method = request.getMethod(); 109 | setURI(URI.create(request.getUrl())); 110 | 111 | // Add all headers. 112 | for (Header header : request.getHeaders()) { 113 | addHeader(new BasicHeader(header.getName(), header.getValue())); 114 | } 115 | } 116 | 117 | @Override 118 | public String getMethod() { 119 | return method; 120 | } 121 | } 122 | 123 | private static class GenericEntityHttpRequest extends HttpEntityEnclosingRequestBase { 124 | private final String method; 125 | 126 | GenericEntityHttpRequest(Request request, RequestCallback requestCallback) { 127 | super(); 128 | method = request.getMethod(); 129 | setURI(URI.create(request.getUrl())); 130 | 131 | // Add all headers. 132 | for (Header header : request.getHeaders()) { 133 | addHeader(new BasicHeader(header.getName(), header.getValue())); 134 | } 135 | 136 | // Add the content body. 137 | setEntity(new TypedOutputEntity(request.getBody(), requestCallback)); 138 | } 139 | 140 | @Override 141 | public String getMethod() { 142 | return method; 143 | } 144 | } 145 | 146 | static class TypedOutputEntity extends AbstractHttpEntity { 147 | private final ActionBody requestBody; 148 | private final RequestCallback requestCallback; 149 | 150 | TypedOutputEntity(ActionBody requestBody, RequestCallback requestCallback) { 151 | this.requestBody = requestBody; 152 | this.requestCallback = requestCallback; 153 | setContentType(requestBody.mimeType()); 154 | } 155 | 156 | @Override 157 | public boolean isRepeatable() { 158 | return true; 159 | } 160 | 161 | @Override 162 | public long getContentLength() { 163 | return requestBody.length(); 164 | } 165 | 166 | @Override 167 | public InputStream getContent() throws IOException { 168 | return requestBody.getContent(); 169 | } 170 | 171 | @Override 172 | public void writeTo(OutputStream out) throws IOException { 173 | requestBody.writeContentTo(new ProgressOutputStream(out, new ProgressOutputStream.ProgressListener() { 174 | @Override public void onProgressChanged(long bytesWritten) { 175 | requestCallback.onProgress((int) ((bytesWritten * 100) / getContentLength())); 176 | } 177 | })); 178 | } 179 | 180 | @Override 181 | public boolean isStreaming() { 182 | return false; 183 | } 184 | } 185 | 186 | private static class ResponseActionBody extends ActionBody { 187 | 188 | private HttpEntity body; 189 | 190 | public ResponseActionBody(HttpEntity body) { 191 | super(body.getContentType() == null ? null : body.getContentType().getValue()); 192 | this.body = body; 193 | } 194 | 195 | @Override public long length() { 196 | return body.getContentLength(); 197 | } 198 | 199 | @Override public InputStream getContent() throws IOException { 200 | return body.getContent(); 201 | } 202 | 203 | @Override public void writeContentTo(OutputStream os) throws IOException { 204 | body.writeTo(os); 205 | } 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /clients/client-okhttp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply from: rootProject.file('gradle/plugins/maven-simple.gradle') 3 | 4 | compileJava { 5 | sourceCompatibility = '1.6' 6 | targetCompatibility = '1.6' 7 | } 8 | 9 | dependencies { 10 | api project(':service') 11 | api 'com.squareup.okhttp:okhttp:2.7.5' 12 | } 13 | -------------------------------------------------------------------------------- /clients/client-okhttp/src/main/java/io/techery/janet/okhttp/OkClient.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.okhttp; 2 | 3 | import com.squareup.okhttp.Call; 4 | import com.squareup.okhttp.MediaType; 5 | import com.squareup.okhttp.OkHttpClient; 6 | import com.squareup.okhttp.RequestBody; 7 | import com.squareup.okhttp.ResponseBody; 8 | import com.squareup.okhttp.internal.Util; 9 | 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.io.OutputStream; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import io.techery.janet.body.ActionBody; 18 | import io.techery.janet.body.util.StreamUtil; 19 | import io.techery.janet.http.HttpClient; 20 | import io.techery.janet.http.internal.ProgressOutputStream; 21 | import io.techery.janet.http.internal.ProgressOutputStream.ProgressListener; 22 | import io.techery.janet.http.model.Header; 23 | import io.techery.janet.http.model.Request; 24 | import io.techery.janet.http.model.Response; 25 | import io.techery.janet.http.utils.RequestUtils; 26 | import okio.BufferedSink; 27 | 28 | public class OkClient implements HttpClient { 29 | 30 | private static OkHttpClient defaultOkHttp() { 31 | OkHttpClient client = new OkHttpClient(); 32 | client.setConnectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 33 | client.setReadTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); 34 | return client; 35 | } 36 | 37 | private final com.squareup.okhttp.OkHttpClient client; 38 | 39 | public OkClient() { 40 | this(defaultOkHttp()); 41 | } 42 | 43 | public OkClient(com.squareup.okhttp.OkHttpClient okHttpClient) { 44 | this.client = okHttpClient; 45 | } 46 | 47 | @Override public Response execute(Request request, final RequestCallback requestCallback) throws IOException { 48 | com.squareup.okhttp.Request.Builder okRequestBuilder = new com.squareup.okhttp.Request.Builder(); 49 | okRequestBuilder.url(request.getUrl()); 50 | for (Header header : request.getHeaders()) { 51 | okRequestBuilder.addHeader(header.getName(), header.getValue()); 52 | } 53 | ActionRequestBody requestBody = null; 54 | final ActionBody actionBody = request.getBody(); 55 | if (actionBody != null) { 56 | requestBody = new ActionRequestBody(actionBody, new ProgressListener() { 57 | @Override public void onProgressChanged(long bytesWritten) { 58 | requestCallback.onProgress((int) ((bytesWritten * 100) / actionBody.length())); 59 | } 60 | }); 61 | } 62 | com.squareup.okhttp.Request okRequest = okRequestBuilder.method(request.getMethod(), requestBody).build(); 63 | RequestUtils.throwIfCanceled(request); 64 | Call call = client.newCall(okRequest); 65 | request.tag = call; //mark for cancellation 66 | com.squareup.okhttp.Response okResponse = call.execute(); 67 | List
responseHeaders = new ArrayList
(); 68 | for (String headerName : okResponse.headers().names()) { 69 | responseHeaders.add(new Header(headerName, okResponse.header(headerName))); 70 | } 71 | ActionBody responseBody = null; 72 | if (okResponse.body() != null) { 73 | responseBody = new ResponseActionBody(okResponse.body()); 74 | } 75 | return new Response( 76 | okResponse.request().url().toString(), 77 | okResponse.code(), okResponse.message(), responseHeaders, responseBody 78 | ); 79 | } 80 | 81 | @Override public void cancel(Request request) { 82 | if (request.tag != null && (request.tag instanceof Call)) { 83 | Call call = (Call) request.tag; 84 | call.cancel(); 85 | } 86 | request.tag = RequestUtils.TAG_CANCELED; 87 | } 88 | 89 | private static class ActionRequestBody extends RequestBody { 90 | 91 | private final ActionBody actionBody; 92 | private final ProgressListener listener; 93 | 94 | private ActionRequestBody(ActionBody actionBody, ProgressListener progressListener) { 95 | this.actionBody = actionBody; 96 | this.listener = progressListener; 97 | } 98 | 99 | @Override public MediaType contentType() { 100 | return MediaType.parse(actionBody.mimeType()); 101 | } 102 | 103 | @Override public void writeTo(BufferedSink sink) throws IOException { 104 | OutputStream stream = new ProgressOutputStream(sink.outputStream(), listener, HttpClient.PROGRESS_THRESHOLD); 105 | try { 106 | actionBody.writeContentTo(stream); 107 | stream.flush(); 108 | } finally { 109 | Util.closeQuietly(stream); 110 | } 111 | } 112 | 113 | @Override public long contentLength() throws IOException { 114 | return actionBody.length(); 115 | } 116 | } 117 | 118 | private static class ResponseActionBody extends ActionBody { 119 | 120 | private ResponseBody body; 121 | 122 | public ResponseActionBody(ResponseBody body) { 123 | super(body.contentType() == null ? null : body.contentType().toString()); 124 | this.body = body; 125 | } 126 | 127 | @Override public long length() throws RuntimeException { 128 | try { 129 | return body.contentLength(); 130 | } catch (IOException e) { 131 | throw new RuntimeException("Can't get content length from response", e); 132 | } 133 | } 134 | 135 | @Override public InputStream getContent() throws IOException { 136 | return body.byteStream(); 137 | } 138 | 139 | @Override public void writeContentTo(OutputStream os) throws IOException { 140 | StreamUtil.writeAll(body.byteStream(), os, StreamUtil.NETWORK_CHUNK_SIZE); 141 | } 142 | } 143 | 144 | } 145 | -------------------------------------------------------------------------------- /clients/client-okhttp3/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply from: rootProject.file('gradle/plugins/maven-simple.gradle') 3 | 4 | compileJava { 5 | sourceCompatibility = '1.6' 6 | targetCompatibility = '1.6' 7 | } 8 | 9 | dependencies { 10 | api project(':service') 11 | api 'com.squareup.okhttp3:okhttp:3.7.0' 12 | } 13 | -------------------------------------------------------------------------------- /clients/client-okhttp3/src/main/java/io/techery/janet/okhttp3/OkClient.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.okhttp3; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | import io.techery.janet.body.ActionBody; 11 | import io.techery.janet.body.util.StreamUtil; 12 | import io.techery.janet.http.HttpClient; 13 | import io.techery.janet.http.internal.ProgressOutputStream; 14 | import io.techery.janet.http.internal.ProgressOutputStream.ProgressListener; 15 | import io.techery.janet.http.model.Header; 16 | import io.techery.janet.http.model.Request; 17 | import io.techery.janet.http.model.Response; 18 | import io.techery.janet.http.utils.RequestUtils; 19 | import okhttp3.Call; 20 | import okhttp3.MediaType; 21 | import okhttp3.OkHttpClient; 22 | import okhttp3.ResponseBody; 23 | import okhttp3.internal.Util; 24 | import okio.BufferedSink; 25 | 26 | public class OkClient implements HttpClient { 27 | 28 | private static OkHttpClient defaultOkHttp() { 29 | return new OkHttpClient.Builder() 30 | .connectTimeout(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) 31 | .readTimeout(READ_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS) 32 | .build(); 33 | } 34 | 35 | private final OkHttpClient client; 36 | 37 | public OkClient() { 38 | this(defaultOkHttp()); 39 | } 40 | 41 | public OkClient(OkHttpClient okHttpClient) { 42 | this.client = okHttpClient; 43 | } 44 | 45 | @Override public Response execute(Request request, final RequestCallback requestCallback) throws IOException { 46 | okhttp3.Request.Builder okRequestBuilder = new okhttp3.Request.Builder(); 47 | okRequestBuilder.url(request.getUrl()); 48 | for (Header header : request.getHeaders()) { 49 | okRequestBuilder.addHeader(header.getName(), header.getValue()); 50 | } 51 | ActionRequestBody requestBody = null; 52 | final ActionBody actionBody = request.getBody(); 53 | if (actionBody != null) { 54 | requestBody = new ActionRequestBody(actionBody, new ProgressListener() { 55 | @Override public void onProgressChanged(long bytesWritten) { 56 | requestCallback.onProgress((int) ((bytesWritten * 100) / actionBody.length())); 57 | } 58 | }); 59 | } 60 | final okhttp3.Request okRequest = okRequestBuilder.method(request.getMethod(), requestBody).build(); 61 | RequestUtils.throwIfCanceled(request); 62 | Call call = client.newCall(okRequest); 63 | request.tag = call; //mark for cancellation 64 | final okhttp3.Response okResponse = call.execute(); 65 | List
responseHeaders = new ArrayList
(); 66 | for (String headerName : okResponse.headers().names()) { 67 | responseHeaders.add(new Header(headerName, okResponse.header(headerName))); 68 | } 69 | ActionBody responseBody = null; 70 | if (okResponse.body() != null) { 71 | responseBody = new ResponseActionBody(okResponse.body()); 72 | } 73 | return new Response( 74 | okResponse.request().url().toString(), 75 | okResponse.code(), okResponse.message(), responseHeaders, responseBody 76 | ); 77 | } 78 | 79 | @Override public void cancel(Request request) { 80 | if (request.tag != null && (request.tag instanceof Call)) { 81 | Call call = (Call) request.tag; 82 | call.cancel(); 83 | } 84 | request.tag = RequestUtils.TAG_CANCELED; 85 | } 86 | 87 | private static class ActionRequestBody extends okhttp3.RequestBody { 88 | 89 | private final ActionBody actionBody; 90 | private final ProgressListener listener; 91 | 92 | private ActionRequestBody(ActionBody actionBody, ProgressListener progressListener) { 93 | this.actionBody = actionBody; 94 | this.listener = progressListener; 95 | } 96 | 97 | @Override public MediaType contentType() { 98 | return MediaType.parse(actionBody.mimeType()); 99 | } 100 | 101 | @Override public void writeTo(BufferedSink sink) throws IOException { 102 | OutputStream stream = new ProgressOutputStream(sink.outputStream(), listener, HttpClient.PROGRESS_THRESHOLD); 103 | try { 104 | actionBody.writeContentTo(stream); 105 | stream.flush(); 106 | } finally { 107 | Util.closeQuietly(stream); 108 | } 109 | } 110 | 111 | @Override public long contentLength() throws IOException { 112 | return actionBody.length(); 113 | } 114 | } 115 | 116 | private static class ResponseActionBody extends ActionBody { 117 | 118 | private final ResponseBody body; 119 | 120 | public ResponseActionBody(ResponseBody body) { 121 | super(body.contentType() == null ? null : body.contentType().toString()); 122 | this.body = body; 123 | } 124 | 125 | @Override public long length() { 126 | return body.contentLength(); 127 | } 128 | 129 | @Override public InputStream getContent() throws IOException { 130 | return body.byteStream(); 131 | } 132 | 133 | @Override public void writeContentTo(OutputStream os) throws IOException { 134 | StreamUtil.writeAll(body.byteStream(), os, StreamUtil.NETWORK_CHUNK_SIZE); 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /clients/client-url-connection/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply from: rootProject.file('gradle/plugins/maven-simple.gradle') 3 | 4 | compileJava { 5 | sourceCompatibility = '1.6' 6 | targetCompatibility = '1.6' 7 | } 8 | 9 | dependencies { 10 | api project(':service') 11 | } 12 | -------------------------------------------------------------------------------- /clients/client-url-connection/src/main/java/io/techery/janet/http/UrlConnectionClient.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.net.HttpURLConnection; 7 | import java.net.URL; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import io.techery.janet.body.ActionBody; 13 | import io.techery.janet.body.util.StreamUtil; 14 | import io.techery.janet.http.internal.ProgressOutputStream; 15 | import io.techery.janet.http.model.Header; 16 | import io.techery.janet.http.model.Request; 17 | import io.techery.janet.http.model.Response; 18 | import io.techery.janet.http.utils.RequestUtils; 19 | 20 | 21 | public class UrlConnectionClient implements HttpClient { 22 | private static final int CHUNK_SIZE = 4096; 23 | private static final int BUFFER_SIZE = 0x1000; 24 | 25 | public UrlConnectionClient() {} 26 | 27 | @Override public Response execute(Request request, RequestCallback requestCallback) throws IOException { 28 | HttpURLConnection connection = openConnection(request); 29 | RequestUtils.throwIfCanceled(request); 30 | request.tag = connection; 31 | writeRequest(connection, request, requestCallback); 32 | RequestUtils.throwIfCanceled(request); 33 | return readResponse(connection); 34 | } 35 | 36 | @Override public void cancel(Request request) { 37 | if (request.tag != null && (request.tag instanceof HttpURLConnection)) { 38 | HttpURLConnection connection = (HttpURLConnection) request.tag; 39 | connection.disconnect(); 40 | if (connection.getDoOutput()) { 41 | try { 42 | connection.getOutputStream().close(); 43 | } catch (IOException ignored) {} 44 | } 45 | if (connection.getDoInput()) { 46 | try { 47 | connection.getInputStream().close(); 48 | } catch (IOException ignored) {} 49 | } 50 | } 51 | request.tag = RequestUtils.TAG_CANCELED; //mark request as canceled 52 | } 53 | 54 | protected HttpURLConnection openConnection(Request request) throws IOException { 55 | HttpURLConnection connection = (HttpURLConnection) new URL(request.getUrl()).openConnection(); 56 | connection.setConnectTimeout(HttpClient.CONNECT_TIMEOUT_MILLIS); 57 | connection.setReadTimeout(HttpClient.READ_TIMEOUT_MILLIS); 58 | return connection; 59 | } 60 | 61 | private void writeRequest(HttpURLConnection connection, Request request, final RequestCallback requestCallback) throws IOException { 62 | connection.setRequestMethod(request.getMethod()); 63 | connection.setDoInput(true); 64 | 65 | for (Header header : request.getHeaders()) { 66 | connection.addRequestProperty(header.getName(), header.getValue()); 67 | } 68 | 69 | ActionBody body = request.getBody(); 70 | if (body != null) { 71 | connection.setDoOutput(true); 72 | connection.addRequestProperty("Content-Type", body.mimeType()); 73 | OutputStream outputStream; 74 | final long length = body.length(); 75 | if (length != -1) { 76 | connection.setFixedLengthStreamingMode((int) length); 77 | connection.addRequestProperty("Content-Length", String.valueOf(length)); 78 | outputStream = new ProgressOutputStream(connection.getOutputStream(), new ProgressOutputStream.ProgressListener() { 79 | @Override public void onProgressChanged(long bytesWritten) { 80 | requestCallback.onProgress((int) ((bytesWritten * 100) / length)); 81 | } 82 | }); 83 | } else { 84 | outputStream = connection.getOutputStream(); 85 | connection.setChunkedStreamingMode(CHUNK_SIZE); 86 | } 87 | body.writeContentTo(outputStream); 88 | } 89 | } 90 | 91 | Response readResponse(HttpURLConnection connection) throws IOException { 92 | int status = connection.getResponseCode(); 93 | String reason = connection.getResponseMessage(); 94 | if (reason == null) reason = ""; // HttpURLConnection treats empty reason as null. 95 | 96 | List
headers = new ArrayList
(); 97 | for (Map.Entry> field : connection.getHeaderFields().entrySet()) { 98 | String name = field.getKey(); 99 | for (String value : field.getValue()) { 100 | headers.add(new Header(name, value)); 101 | } 102 | } 103 | ActionBody responseBody = new ResponseActionBody(connection); 104 | return new Response(connection.getURL().toString(), status, reason, headers, responseBody); 105 | } 106 | 107 | private static class ResponseActionBody extends ActionBody { 108 | 109 | private HttpURLConnection body; 110 | 111 | public ResponseActionBody(HttpURLConnection body) { 112 | super(body.getContentType()); 113 | this.body = body; 114 | } 115 | 116 | @Override public long length() { 117 | return body.getContentLengthLong(); 118 | } 119 | 120 | @Override public InputStream getContent() throws IOException { 121 | int status = body.getResponseCode(); 122 | if (status >= 200 && status < 300) { 123 | return body.getInputStream(); 124 | } else { 125 | return body.getErrorStream(); 126 | } 127 | } 128 | 129 | @Override public void writeContentTo(OutputStream os) throws IOException { 130 | StreamUtil.writeAll(getContent(), os, StreamUtil.NETWORK_CHUNK_SIZE); 131 | } 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /gradle/plugins/maven-simple.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | 3 | task sourcesJar(type: Jar, dependsOn: classes) { 4 | classifier = 'sources' 5 | from sourceSets.main.allSource 6 | } 7 | 8 | task javadocJar(type: Jar, dependsOn: javadoc) { 9 | classifier = 'javadoc' 10 | from javadoc.destinationDir 11 | } 12 | 13 | artifacts { 14 | archives sourcesJar 15 | archives javadocJar 16 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techery/janet-http/c54b526f94873beb1617a65ab3b9efd8b08664b4/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 28 10:00:20 PST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin = "1.2.41" 3 | repositories { 4 | jcenter() 5 | maven { 6 | url "https://plugins.gradle.org/m2/" 7 | } 8 | } 9 | dependencies { 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin" 11 | } 12 | } 13 | 14 | apply plugin: 'application' 15 | apply plugin: 'kotlin' 16 | apply plugin: 'kotlin-kapt' 17 | apply plugin: 'idea' 18 | 19 | compileJava { 20 | sourceCompatibility = '1.8' 21 | targetCompatibility = '1.8' 22 | } 23 | 24 | mainClassName = 'io.techery.janet.http.sample.HttpSample' 25 | 26 | dependencies { 27 | compile project(':service') 28 | kapt project(':service-compiler') 29 | compile project(':clients:client-okhttp3') 30 | compile 'com.github.techery.janet-converters:gson:1.1.1' 31 | compile 'com.google.code.gson:gson:2.8.0' 32 | //kotlin 33 | compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin" 34 | compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin" 35 | compile 'io.reactivex:rxkotlin:1.0.0' 36 | } 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/HttpSample.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.security.KeyManagementException; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.security.cert.CertificateException; 8 | 9 | import javax.net.ssl.HostnameVerifier; 10 | import javax.net.ssl.SSLContext; 11 | import javax.net.ssl.SSLSession; 12 | import javax.net.ssl.SSLSocketFactory; 13 | import javax.net.ssl.TrustManager; 14 | import javax.net.ssl.X509TrustManager; 15 | 16 | import io.techery.janet.ActionPipe; 17 | import io.techery.janet.HttpActionService; 18 | import io.techery.janet.Janet; 19 | import io.techery.janet.gson.GsonConverter; 20 | import io.techery.janet.helper.ActionStateSubscriber; 21 | import io.techery.janet.http.sample.action.TestProgressAction; 22 | import io.techery.janet.http.sample.action.UserReposAction; 23 | import io.techery.janet.http.sample.action.UsersAction; 24 | import io.techery.janet.http.sample.action.base.BaseAction; 25 | import io.techery.janet.http.sample.model.User; 26 | import io.techery.janet.http.sample.util.SampleLoggingService; 27 | import io.techery.janet.okhttp3.OkClient; 28 | import okhttp3.OkHttpClient; 29 | import rx.Observable; 30 | 31 | public class HttpSample { 32 | 33 | private static final String API_URL = "https://api.github.com"; 34 | 35 | public static void main(String... args) throws NoSuchAlgorithmException, KeyManagementException { 36 | OkClient httpClient = new OkClient(createTrustingOkHttpClient()); 37 | 38 | Janet janet = new Janet.Builder() 39 | .addService(new SampleLoggingService(new HttpActionService(API_URL, httpClient, new GsonConverter(new Gson())))) 40 | .build(); 41 | 42 | ActionPipe usersPipe = janet.createPipe(UsersAction.class); 43 | ActionPipe userReposPipe = janet.createPipe(UserReposAction.class); 44 | 45 | usersPipe.observeSuccess() 46 | .filter(BaseAction::isSuccess) 47 | .subscribe( 48 | action -> System.out.println("received " + action), 49 | System.err::println 50 | ); 51 | 52 | usersPipe.send(new UsersAction()); 53 | 54 | usersPipe.createObservable(new UsersAction()) 55 | .filter(state -> state.action.isSuccess()) 56 | .flatMap(state -> Observable.from(state.action.getResponse()).first()) 57 | .flatMap(user -> userReposPipe.createObservable(new UserReposAction(user.getLogin()))) 58 | .subscribe(new ActionStateSubscriber() 59 | .onSuccess(action -> System.out.println("repos request finished " + action)) 60 | .onFail((action, throwable) -> System.err.println("repos request exception " + throwable)) 61 | ); 62 | 63 | 64 | janet = new Janet.Builder() 65 | .addService(new SampleLoggingService(new HttpActionService("http://httpbin.org", httpClient, new GsonConverter(new Gson())))) 66 | .build(); 67 | 68 | janet.createPipe(TestProgressAction.class) 69 | .createObservable(new TestProgressAction()) 70 | .subscribe(new ActionStateSubscriber() 71 | .onSuccess(action -> System.out.println("request finished " + action)) 72 | .onProgress((action, progress) -> System.out.println(String.format("progress value:%s", progress)))); 73 | 74 | } 75 | 76 | private static OkHttpClient createTrustingOkHttpClient() throws NoSuchAlgorithmException, KeyManagementException { 77 | // Create a trust manager that does not validate certificate chains 78 | final TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() { 79 | @Override 80 | public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) 81 | throws CertificateException { 82 | } 83 | 84 | @Override 85 | public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) 86 | throws CertificateException { 87 | } 88 | 89 | @Override 90 | public java.security.cert.X509Certificate[] getAcceptedIssuers() { 91 | return new java.security.cert.X509Certificate[]{}; 92 | } 93 | }}; 94 | // Install the all-trusting trust manager 95 | final SSLContext sslContext = SSLContext.getInstance("SSL"); 96 | sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); 97 | // Create an ssl socket factory with our all-trusting manager 98 | final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); 99 | OkHttpClient.Builder builder = new OkHttpClient.Builder(); 100 | builder.sslSocketFactory(sslSocketFactory); 101 | builder.hostnameVerifier(new HostnameVerifier() { 102 | @Override 103 | public boolean verify(String hostname, SSLSession session) { 104 | return true; 105 | } 106 | }); 107 | return builder.build(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/action/TestProgressAction.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample.action; 2 | 3 | import io.techery.janet.body.StringBody; 4 | import io.techery.janet.http.annotations.HttpAction; 5 | import io.techery.janet.http.annotations.Part; 6 | import io.techery.janet.http.annotations.Query; 7 | import io.techery.janet.http.annotations.Response; 8 | import io.techery.janet.http.model.MultipartRequestBody; 9 | import io.techery.janet.http.sample.action.base.BaseAction; 10 | 11 | @HttpAction(method = HttpAction.Method.POST, value = "/post", type = HttpAction.Type.MULTIPART) 12 | public class TestProgressAction extends BaseAction { 13 | 14 | @Query("dir") final String dirParam = "janet"; 15 | 16 | @Part("part_name1") final MultipartRequestBody.PartBody part1; 17 | 18 | @Part("part_name2") final String part2; 19 | 20 | @Part("part_name3") final String part3; 21 | 22 | @Response String response; 23 | 24 | public TestProgressAction() { 25 | part1 = new MultipartRequestBody.PartBody.Builder() 26 | .setBody(new StringBody(CONTENT)) 27 | .addHeader("filename", "content") 28 | .addHeader("X-Custom-Header", "blablabla") 29 | .build(); 30 | part2 = "some_string_data"; 31 | part3 = "another_string_data"; 32 | } 33 | 34 | public String getResponse() { 35 | return response; 36 | } 37 | 38 | @Override public String toString() { 39 | return "TestProgressAction{" + 40 | "dirParam='" + dirParam + '\'' + 41 | ", part1=" + part1 + 42 | ", part2='" + part2 + '\'' + 43 | ", part3='" + part3 + '\'' + 44 | ", response='" + response + '\'' + 45 | "} " + super.toString(); 46 | } 47 | 48 | //the large string instead of a file 49 | private final static String CONTENT = "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test" + 50 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 51 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 52 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 53 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 54 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 55 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 56 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 57 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 58 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 59 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 60 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 61 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 62 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 63 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 64 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 65 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 66 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 67 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 68 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 69 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 70 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 71 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 72 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 73 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 74 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 75 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 76 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 77 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 78 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 79 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 80 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 81 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 82 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 83 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 84 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 85 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 86 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 87 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 88 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 89 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 90 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 91 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 92 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 93 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 94 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 95 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 96 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 97 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 98 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 99 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 100 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 101 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 102 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 103 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 104 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 105 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 106 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 107 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 108 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 109 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 110 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 111 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 112 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 113 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 114 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test" + 115 | "test-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-testtest-test-test-test-test-test-test-test-test"; 116 | } 117 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/action/UserReposAction.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample.action; 2 | 3 | 4 | import java.util.ArrayList; 5 | 6 | import io.techery.janet.http.annotations.HttpAction; 7 | import io.techery.janet.http.annotations.Path; 8 | import io.techery.janet.http.annotations.Response; 9 | import io.techery.janet.http.sample.action.base.BaseAction; 10 | import io.techery.janet.http.sample.model.Repository; 11 | 12 | @HttpAction("/users/{login}/repos") 13 | public class UserReposAction extends BaseAction { 14 | 15 | @Path("login") final String login; 16 | 17 | @Response ArrayList repositories; 18 | 19 | public UserReposAction(String login) { 20 | this.login = login; 21 | } 22 | 23 | public ArrayList getResponse() { 24 | return repositories; 25 | } 26 | 27 | @Override public String toString() { 28 | return "UserReposAction{" + 29 | "login='" + login + '\'' + 30 | ", repositories=" + repositories + 31 | '}'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/action/UsersAction.kt: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample.action 2 | 3 | 4 | import java.util.ArrayList 5 | 6 | import io.techery.janet.http.annotations.HttpAction 7 | import io.techery.janet.http.annotations.Query 8 | import io.techery.janet.http.annotations.Response 9 | import io.techery.janet.http.sample.action.base.BaseAction 10 | import io.techery.janet.http.sample.model.User 11 | 12 | @HttpAction("/users") 13 | data class UsersAction(@Query("since") val since: Int = 0) : BaseAction() { 14 | 15 | @Response 16 | lateinit var response: ArrayList 17 | 18 | override fun toString(): String { 19 | return "UsersAction(since=$since${if (::response.isInitialized) ", response=$response" else ""})" 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/action/base/BaseAction.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample.action.base; 2 | 3 | 4 | import io.techery.janet.http.annotations.Status; 5 | 6 | /** 7 | * This action class was created to show, 8 | * that action helper will be generated to fill the 9 | * annotated variables of super class too. 10 | */ 11 | public abstract class BaseAction extends ServerAction { 12 | 13 | @Status 14 | boolean success; 15 | 16 | public boolean isSuccess() { 17 | return success; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/action/base/ServerAction.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample.action.base; 2 | 3 | public abstract class ServerAction { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/model/Repository.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample.model; 2 | 3 | public class Repository { 4 | 5 | String name; 6 | 7 | String description; 8 | 9 | 10 | public String getName() { 11 | return name; 12 | } 13 | 14 | public String getDescription() { 15 | return description; 16 | } 17 | 18 | @Override 19 | public boolean equals(Object o) { 20 | if (this == o) return true; 21 | if (o == null || getClass() != o.getClass()) return false; 22 | 23 | Repository that = (Repository) o; 24 | 25 | if (name != null ? !name.equals(that.name) : that.name != null) return false; 26 | return !(description != null ? !description.equals(that.description) : that.description != null); 27 | 28 | } 29 | 30 | @Override 31 | public int hashCode() { 32 | int result = name != null ? name.hashCode() : 0; 33 | result = 31 * result + (description != null ? description.hashCode() : 0); 34 | return result; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return "Repository{" + 40 | "name='" + name + '\'' + 41 | ", description='" + description + '\'' + 42 | '}'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/model/User.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample.model; 2 | 3 | public class User { 4 | 5 | Long id; 6 | 7 | String login; 8 | 9 | public Long getId() { 10 | return id; 11 | } 12 | 13 | public String getLogin() { 14 | return login; 15 | } 16 | 17 | @Override 18 | public boolean equals(Object o) { 19 | if (this == o) return true; 20 | if (o == null || getClass() != o.getClass()) return false; 21 | 22 | User user = (User) o; 23 | 24 | if (id != null ? !id.equals(user.id) : user.id != null) return false; 25 | return !(login != null ? !login.equals(user.login) : user.login != null); 26 | 27 | } 28 | 29 | @Override 30 | public int hashCode() { 31 | int result = id != null ? id.hashCode() : 0; 32 | result = 31 * result + (login != null ? login.hashCode() : 0); 33 | return result; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "User{" + 39 | "id=" + id + 40 | ", login='" + login + '\'' + 41 | '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample/src/main/java/io/techery/janet/http/sample/util/SampleLoggingService.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample.util; 2 | 3 | import io.techery.janet.ActionHolder; 4 | import io.techery.janet.ActionService; 5 | import io.techery.janet.ActionServiceWrapper; 6 | import io.techery.janet.JanetException; 7 | 8 | public class SampleLoggingService extends ActionServiceWrapper { 9 | 10 | public SampleLoggingService(ActionService actionService) { 11 | super(actionService); 12 | } 13 | 14 | @Override protected boolean onInterceptSend(ActionHolder holder) { 15 | System.out.println("send " + holder.action()); 16 | return false; 17 | } 18 | 19 | @Override protected void onInterceptCancel(ActionHolder holder) { 20 | System.out.println("cancel " + holder.action()); 21 | } 22 | 23 | @Override protected void onInterceptStart(ActionHolder holder) { 24 | System.out.println("onStart " + holder.action()); 25 | } 26 | 27 | @Override protected void onInterceptProgress(ActionHolder holder, int progress) { 28 | System.out.println("onProgress " + holder.action() + ", progress " + progress); 29 | } 30 | 31 | @Override protected void onInterceptSuccess(ActionHolder holder) { 32 | System.out.println("onSuccess " + holder.action()); 33 | } 34 | 35 | @Override protected boolean onInterceptFail(ActionHolder holder, JanetException e) { 36 | System.out.println("onFail " + holder.action()); 37 | e.printStackTrace(); 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/io/techery/janet/http/sample/KSimpleService.kt: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.sample 2 | 3 | import com.google.gson.Gson 4 | import io.techery.janet.HttpActionService 5 | import io.techery.janet.Janet 6 | import io.techery.janet.gson.GsonConverter 7 | import io.techery.janet.helper.ActionStateSubscriber 8 | import io.techery.janet.http.sample.action.UserReposAction 9 | import io.techery.janet.http.sample.action.UsersAction 10 | import io.techery.janet.okhttp3.OkClient 11 | import rx.Observable 12 | 13 | const private val API_URL = "https://api.github.com" 14 | 15 | 16 | fun main(args: Array) { 17 | 18 | val janet = Janet.Builder() 19 | .addService(HttpActionService(API_URL, OkClient(), GsonConverter(Gson()))) 20 | .build() 21 | 22 | val usersPipe = janet.createPipe(UsersAction::class.java) 23 | val userReposPipe = janet.createPipe(UserReposAction::class.java) 24 | 25 | usersPipe.observeSuccess() 26 | .filter({ it.isSuccess }) 27 | .subscribe({ println("received $it") }) { println(it) } 28 | 29 | usersPipe.createObservable(UsersAction()) 30 | .filter { it.action.isSuccess } 31 | .flatMap { Observable.from(it.action.response).first() } 32 | .flatMap { userReposPipe.createObservable(UserReposAction(it.login)) } 33 | .subscribe(ActionStateSubscriber() 34 | .onSuccess { println("repos request finished $it") } 35 | .onFail { _, t -> println("repos request exception $t") } 36 | ) 37 | 38 | 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /service-compiler/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply from: rootProject.file('gradle/plugins/maven-simple.gradle') 3 | 4 | compileJava { 5 | sourceCompatibility = '1.6' 6 | targetCompatibility = '1.6' 7 | } 8 | 9 | dependencies { 10 | compile project(':service') 11 | compile 'com.github.techery:janet-service-compiler-utils:1.0.1' 12 | compile 'com.squareup:javapoet:1.1.0' 13 | compile 'com.google.guava:guava:18.0' 14 | compile 'com.google.auto.service:auto-service:1.0-rc2' 15 | compile 'com.google.code.findbugs:jsr305:2.0.3' 16 | compile 'org.apache.velocity:velocity:1.7' 17 | compile 'org.ow2.asm:asm:4.1' 18 | } 19 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/HttpActionClass.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet; 2 | 3 | import com.squareup.javapoet.ClassName; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Iterator; 7 | import java.util.List; 8 | 9 | import javax.lang.model.element.Element; 10 | import javax.lang.model.element.TypeElement; 11 | import javax.lang.model.util.Elements; 12 | 13 | import io.techery.janet.compiler.utils.ActionClass; 14 | import io.techery.janet.http.annotations.HttpAction; 15 | 16 | public class HttpActionClass extends ActionClass { 17 | private HttpAction.Method method; 18 | private String path; 19 | private HttpAction.Type requestType; 20 | private HttpActionClass parent; 21 | private boolean helperExists; 22 | 23 | public HttpActionClass(Elements elementUtils, TypeElement typeElement, HttpActionClass parent) { 24 | super(HttpAction.class, elementUtils, typeElement); 25 | this.parent = parent; 26 | HttpAction annotation = typeElement.getAnnotation(HttpAction.class); 27 | if (annotation != null) { 28 | method = annotation.method(); 29 | path = annotation.value(); 30 | requestType = annotation.type(); 31 | } 32 | helperExists = getHelperElement(elementUtils) != null; 33 | } 34 | 35 | public boolean isAnnotatedClass() { 36 | return method != null && path != null && requestType != null; 37 | } 38 | 39 | public HttpActionClass getParent() { 40 | return parent; 41 | } 42 | 43 | public HttpAction.Method getMethod() { 44 | return method; 45 | } 46 | 47 | public String getPath() { 48 | return path; 49 | } 50 | 51 | public HttpAction.Type getRequestType() { 52 | return requestType; 53 | } 54 | 55 | public ClassName getHelperName() { 56 | return ClassName.get(getPackageName(), getTypeElement().getSimpleName() + HttpHelpersGenerator.HELPER_SUFFIX); 57 | } 58 | 59 | private TypeElement getHelperElement(Elements elementUtils) { 60 | ClassName helperName = getHelperName(); 61 | return elementUtils.getTypeElement(helperName.packageName() + "." + helperName.simpleName()); 62 | } 63 | 64 | public boolean helperExists() { 65 | return helperExists; 66 | } 67 | 68 | @Override public List getAnnotatedElements(Class annotationClass) { 69 | List elements = super.getAnnotatedElements(annotationClass); 70 | for (Iterator iterator = elements.iterator(); iterator.hasNext(); ) { 71 | Element element = iterator.next(); 72 | if (!element.getEnclosingElement().equals(getTypeElement())) { 73 | iterator.remove(); 74 | } 75 | } 76 | return elements; 77 | } 78 | 79 | public List getAllAnnotatedElements(Class annotationClass) { 80 | List elements = new ArrayList(getAnnotatedElements(annotationClass)); 81 | if (parent != null) { 82 | elements.addAll(parent.getAllAnnotatedElements(annotationClass)); 83 | } 84 | return elements; 85 | } 86 | 87 | @Override public String toString() { 88 | return getTypeElement() + "{" + 89 | "method=" + method + 90 | ", path='" + path + '\'' + 91 | ", requestType=" + requestType + 92 | ", parent=" + parent + 93 | '}'; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/HttpHelpersFactoryGenerator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet; 2 | 3 | import com.squareup.javapoet.ClassName; 4 | import com.squareup.javapoet.CodeBlock; 5 | import com.squareup.javapoet.FieldSpec; 6 | import com.squareup.javapoet.MethodSpec; 7 | import com.squareup.javapoet.ParameterizedTypeName; 8 | import com.squareup.javapoet.TypeSpec; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import javax.annotation.processing.Filer; 14 | import javax.lang.model.element.Modifier; 15 | import javax.lang.model.element.TypeElement; 16 | 17 | import io.techery.janet.HttpActionService.ActionHelper; 18 | import io.techery.janet.compiler.utils.Generator; 19 | 20 | import static com.squareup.javapoet.MethodSpec.constructorBuilder; 21 | import static com.squareup.javapoet.MethodSpec.methodBuilder; 22 | import static com.squareup.javapoet.TypeSpec.classBuilder; 23 | 24 | public class HttpHelpersFactoryGenerator extends Generator { 25 | 26 | private final List otherFactoriesElements; 27 | private final Options options; 28 | 29 | public HttpHelpersFactoryGenerator(Filer filer, List otherFactories, Options options) { 30 | super(filer); 31 | this.otherFactoriesElements = otherFactories; 32 | this.options = options; 33 | } 34 | 35 | @Override 36 | public void generate(ArrayList actionClasses) { 37 | String className = HttpActionService.HELPERS_FACTORY_CLASS_SIMPLE_NAME; 38 | if (options.factoryClassSuffix != null) className += options.factoryClassSuffix; 39 | 40 | TypeSpec.Builder classBuilder = classBuilder(className) 41 | .addModifiers(Modifier.PUBLIC, Modifier.FINAL) 42 | .addJavadoc("Auto-generated class factory to create action helpers for actions") 43 | .addSuperinterface(ParameterizedTypeName.get(HttpActionService.ActionHelperFactory.class)); 44 | 45 | MethodSpec.Builder constructorBuilder = constructorBuilder() 46 | .addModifiers(Modifier.PUBLIC); 47 | 48 | MethodSpec.Builder makeMethodBuilder = methodBuilder("make") 49 | .addModifiers(Modifier.PUBLIC) 50 | .addAnnotation(Override.class) 51 | .returns(ActionHelper.class) 52 | .addParameter(Class.class, "actionClass"); 53 | 54 | if (shouldIncludeOtherFactories()) { 55 | // add field for other factories and initialize it in constructor 56 | ParameterizedTypeName listType = ParameterizedTypeName.get(ArrayList.class, HttpActionService.ActionHelperFactory.class); 57 | classBuilder.addField( 58 | FieldSpec.builder(listType, "otherFactories", Modifier.PRIVATE) 59 | .initializer("new $T()", listType) 60 | .build() 61 | ); 62 | for (TypeElement factoryElement : otherFactoriesElements) { 63 | constructorBuilder.addStatement("otherFactories.add(new $T())", ClassName.get(factoryElement)); 64 | } 65 | // add other factories pre-check 66 | makeMethodBuilder 67 | .beginControlFlow("for ($T factory : otherFactories)", HttpActionService.ActionHelperFactory.class) 68 | .addStatement("$T helper = factory.make(actionClass)", ActionHelper.class) 69 | .addStatement("if (helper != null) return helper") 70 | .endControlFlow(); 71 | } 72 | // factory method logic 73 | for (HttpActionClass actionClass : actionClasses) { 74 | makeMethodBuilder.beginControlFlow("if (actionClass == $T.class)", actionClass.getTypeElement()); 75 | makeMethodBuilder.addCode(createMethodBlock(actionClass)); 76 | makeMethodBuilder.addStatement("return helper"); 77 | makeMethodBuilder.endControlFlow(); 78 | } 79 | makeMethodBuilder.addStatement("return null"); 80 | // 81 | classBuilder.addMethod(constructorBuilder.build()); 82 | classBuilder.addMethod(makeMethodBuilder.build()); 83 | // 84 | saveClass(HttpActionService.HELPERS_FACTORY_CLASS_PACKAGE, classBuilder.build()); 85 | } 86 | 87 | private boolean shouldIncludeOtherFactories() { 88 | return !otherFactoriesElements.isEmpty() && options.factoryClassSuffix == null; 89 | } 90 | 91 | private static CodeBlock createMethodBlock(HttpActionClass actionClass) { 92 | CodeBlock.Builder codeBlockBuilder = CodeBlock.builder(); 93 | if (actionClass.getParent() != null) { 94 | codeBlockBuilder.add(createMethodBlock(actionClass.getParent())); 95 | codeBlockBuilder.addStatement("helper = new $T(($T)helper)", actionClass.getHelperName(), actionClass.getParent() 96 | .getHelperName()); 97 | } else { 98 | codeBlockBuilder.addStatement("$T helper = new $T()", ActionHelper.class, actionClass.getHelperName()); 99 | } 100 | return codeBlockBuilder.build(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/HttpHelpersGenerator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet; 2 | 3 | 4 | import com.squareup.javapoet.ClassName; 5 | import com.squareup.javapoet.CodeBlock; 6 | import com.squareup.javapoet.MethodSpec; 7 | import com.squareup.javapoet.ParameterizedTypeName; 8 | import com.squareup.javapoet.TypeName; 9 | import com.squareup.javapoet.TypeSpec; 10 | import com.squareup.javapoet.TypeVariableName; 11 | 12 | import io.techery.janet.body.ActionBody; 13 | import io.techery.janet.body.BytesArrayBody; 14 | import io.techery.janet.body.FileBody; 15 | import io.techery.janet.body.util.StreamUtil; 16 | import io.techery.janet.compiler.utils.Generator; 17 | import io.techery.janet.compiler.utils.TypeUtils; 18 | import io.techery.janet.converter.Converter; 19 | import io.techery.janet.converter.ConverterException; 20 | import io.techery.janet.http.annotations.*; 21 | import io.techery.janet.http.model.Header; 22 | import io.techery.janet.http.model.MultipartRequestBody; 23 | import io.techery.janet.internal.TypeToken; 24 | import org.apache.commons.lang.StringUtils; 25 | 26 | import java.io.File; 27 | import java.io.IOException; 28 | import java.util.ArrayList; 29 | import java.util.HashSet; 30 | import java.util.List; 31 | import java.util.Set; 32 | 33 | import javax.annotation.processing.Filer; 34 | import javax.lang.model.element.Element; 35 | import javax.lang.model.element.Modifier; 36 | import javax.lang.model.element.TypeElement; 37 | import javax.lang.model.element.TypeParameterElement; 38 | 39 | import io.techery.janet.util.ElementResolver; 40 | 41 | 42 | public class HttpHelpersGenerator extends Generator { 43 | static final String HELPER_SUFFIX = "Helper"; 44 | private static final String BASE_HEADERS_MAP = "headers"; 45 | private static final String PARENT_HELPER_FIELD_NAME = "parent"; 46 | 47 | HttpHelpersGenerator(Filer filer, ElementResolver resolver) { 48 | super(filer); 49 | this.resolver = resolver; 50 | } 51 | 52 | @Override 53 | public void generate(ArrayList actionClasses) { 54 | Set processedElements = new HashSet(); 55 | for (HttpActionClass actionClass : actionClasses) { 56 | while (actionClass != null && !processedElements.contains(actionClass.getTypeElement())) { 57 | if (!actionClass.helperExists()) generateHelper(actionClass); 58 | processedElements.add(actionClass.getTypeElement()); 59 | actionClass = actionClass.getParent(); 60 | } 61 | } 62 | } 63 | 64 | private void generateHelper(HttpActionClass actionClass) { 65 | TypeSpec.Builder classBuilder = TypeSpec.classBuilder(actionClass.getHelperName().simpleName()) 66 | .addModifiers(Modifier.PUBLIC) 67 | .addJavadoc("Janet compile time, autogenerated class, which fills actions") 68 | .addTypeVariables(getTypeVariables(actionClass.getTypeElement())) 69 | .addSuperinterface(ParameterizedTypeName.get( 70 | ClassName.get(HttpActionService.ActionHelper.class), actionClass.getTypeName()) 71 | ); 72 | 73 | if (actionClass.getParent() != null) { 74 | TypeName parentType = actionClass.getParent().getHelperName(); 75 | classBuilder.addField(parentType, PARENT_HELPER_FIELD_NAME, Modifier.PRIVATE, Modifier.FINAL); 76 | classBuilder.addMethod( 77 | MethodSpec.constructorBuilder() 78 | .addModifiers(Modifier.PUBLIC) 79 | .addParameter(parentType, PARENT_HELPER_FIELD_NAME) 80 | .addStatement("this.$L = $L", PARENT_HELPER_FIELD_NAME, PARENT_HELPER_FIELD_NAME) 81 | .build() 82 | ); 83 | } 84 | 85 | classBuilder.addMethod(createFillRequestMethod(actionClass)); 86 | classBuilder.addMethod(createOnResponseMethod(actionClass)); 87 | saveClass(actionClass.getPackageName(), classBuilder.build()); 88 | } 89 | 90 | private MethodSpec createFillRequestMethod(HttpActionClass actionClass) { 91 | MethodSpec.Builder builder = MethodSpec.methodBuilder("fillRequest") 92 | .addModifiers(Modifier.PUBLIC) 93 | .addAnnotation(Override.class) 94 | .returns(RequestBuilder.class) 95 | .addException(ConverterException.class) 96 | .addParameter(RequestBuilder.class, "requestBuilder") 97 | .addParameter(TypeName.get(actionClass.getTypeElement().asType()), "action"); 98 | if (actionClass.isAnnotatedClass()) { 99 | addRequestMethod(actionClass, builder); 100 | addRequestType(actionClass, builder); 101 | addRequestPath(actionClass, builder); 102 | } 103 | if (actionClass.getParent() != null) { 104 | builder.addStatement("$L.fillRequest(requestBuilder, action)", PARENT_HELPER_FIELD_NAME); 105 | } 106 | addRequestUrl(actionClass, builder); 107 | addPathParams(actionClass, builder); 108 | addParts(actionClass, builder); 109 | addRequestHeaders(actionClass, builder); 110 | addRequestFields(actionClass, builder); 111 | addRequestQueries(actionClass, builder); 112 | addRequestBody(actionClass, builder); 113 | builder.addStatement("return requestBuilder"); 114 | return builder.build(); 115 | } 116 | 117 | private void addRequestMethod(HttpActionClass actionClass, MethodSpec.Builder builder) { 118 | builder.addStatement("requestBuilder.setMethod($T.$L)", HttpAction.Method.class, actionClass.getMethod()); 119 | } 120 | 121 | private void addRequestType(HttpActionClass actionClass, MethodSpec.Builder builder) { 122 | builder.addStatement("requestBuilder.setRequestType($T.$L)", HttpAction.Type.class, actionClass.getRequestType()); 123 | } 124 | 125 | private void addRequestPath(HttpActionClass actionClass, MethodSpec.Builder builder) { 126 | if (actionClass.getPath() != null && actionClass.getPath().length() > 0) { 127 | builder.addStatement("requestBuilder.setPath($S)", actionClass.getPath()); 128 | } 129 | } 130 | 131 | private void addRequestUrl(HttpActionClass actionClass, MethodSpec.Builder builder) { 132 | List elements = actionClass.getAnnotatedElements(Url.class); 133 | if (!elements.isEmpty()) { 134 | String accessibleFieldName = resolver.resolveAccessibleFieldNameToRead(actionClass.getTypeElement(), elements.get(0)); 135 | builder.addStatement("requestBuilder.setUrl(action.$L)", accessibleFieldName); 136 | } 137 | } 138 | 139 | private void addRequestFields(HttpActionClass actionClass, MethodSpec.Builder builder) { 140 | for (Element element : actionClass.getAnnotatedElements(Field.class)) { 141 | Field annotation = element.getAnnotation(Field.class); 142 | String accessibleFieldName = resolver.resolveAccessibleFieldNameToRead(actionClass.getTypeElement(), element); 143 | CodeBlock codeBlock = CodeBlock.builder() 144 | .addStatement("requestBuilder.addField($S, action.$L)", annotation.value(), accessibleFieldName) 145 | .build(); 146 | if (!TypeUtils.isPrimitive(element)) { 147 | codeBlock = wrapFieldNotNull(codeBlock, accessibleFieldName); 148 | } 149 | builder.addCode(codeBlock); 150 | } 151 | } 152 | 153 | private void addRequestQueries(HttpActionClass actionClass, MethodSpec.Builder builder) { 154 | for (Element element : actionClass.getAnnotatedElements(Query.class)) { 155 | Query annotation = element.getAnnotation(Query.class); 156 | String accessibleFieldName = resolver.resolveAccessibleFieldNameToRead(actionClass.getTypeElement(), element); 157 | CodeBlock codeBlock = CodeBlock.builder().addStatement( 158 | "requestBuilder.addQueryParam($S, action.$L, $L, $L)", 159 | annotation.value(), accessibleFieldName, annotation.encodeName(), annotation.encodeValue() 160 | ).build(); 161 | if (!TypeUtils.isPrimitive(element)) { 162 | codeBlock = wrapFieldNotNull(codeBlock, accessibleFieldName); 163 | } 164 | builder.addCode(codeBlock); 165 | } 166 | } 167 | 168 | private void addRequestBody(HttpActionClass actionClass, MethodSpec.Builder builder) { 169 | for (Element element : actionClass.getAnnotatedElements(Body.class)) { 170 | String accessibleFieldName = resolver.resolveAccessibleFieldNameToRead(actionClass.getTypeElement(), element); 171 | builder.addStatement("requestBuilder.setBody(action.$L)", accessibleFieldName); 172 | break; 173 | } 174 | } 175 | 176 | private void addRequestHeaders(HttpActionClass actionClass, MethodSpec.Builder builder) { 177 | for (Element element : actionClass.getAnnotatedElements(RequestHeader.class)) { 178 | RequestHeader annotation = element.getAnnotation(RequestHeader.class); 179 | String accessibleFieldName = resolver.resolveAccessibleFieldNameToRead(actionClass.getTypeElement(), element); 180 | CodeBlock codeBlock = CodeBlock.builder() 181 | .addStatement("requestBuilder.addHeader($S, String.valueOf(action.$L))", annotation.value(), accessibleFieldName) 182 | .build(); 183 | if (!TypeUtils.isPrimitive(element)) { 184 | codeBlock = wrapFieldNotNull(codeBlock, accessibleFieldName); 185 | } 186 | builder.addCode(codeBlock); 187 | } 188 | } 189 | 190 | private void addPathParams(HttpActionClass actionClass, MethodSpec.Builder builder) { 191 | for (Element element : actionClass.getAnnotatedElements(Path.class)) { 192 | Path param = element.getAnnotation(Path.class); 193 | String accessibleFieldName = resolver.resolveAccessibleFieldNameToRead(actionClass.getTypeElement(), element); 194 | String path = param.value(); 195 | String name = element.getSimpleName().toString(); 196 | if (StringUtils.isEmpty(path)) { 197 | path = name; 198 | } 199 | boolean encode = param.encode(); 200 | CodeBlock codeBlock = CodeBlock.builder() 201 | .addStatement("requestBuilder.addPathParam($S, String.valueOf(action.$L), $L)", path, accessibleFieldName, encode) 202 | .build(); 203 | if (!TypeUtils.isPrimitive(element)) { 204 | codeBlock = wrapFieldNotNull(codeBlock, accessibleFieldName); 205 | } 206 | builder.addCode(codeBlock); 207 | } 208 | } 209 | 210 | private void addParts(HttpActionClass actionClass, MethodSpec.Builder builder) { 211 | for (Element element : actionClass.getAnnotatedElements(Part.class)) { 212 | Part part = element.getAnnotation(Part.class); 213 | String accessibleFieldName = resolver.resolveAccessibleFieldNameToRead(actionClass.getTypeElement(), element); 214 | String partName = part.value(); 215 | String name = element.getSimpleName().toString(); 216 | if (StringUtils.isEmpty(partName)) { 217 | partName = name; 218 | } 219 | String encode = part.encoding(); 220 | 221 | String bodyFieldName = "partBody"; 222 | CodeBlock.Builder codeBlock = CodeBlock.builder(); 223 | if (TypeUtils.equalType(element, MultipartRequestBody.PartBody.class)) { 224 | codeBlock.addStatement("$T $L = action.$L", 225 | MultipartRequestBody.PartBody.class, bodyFieldName, accessibleFieldName 226 | ); 227 | } else { 228 | String actionBodyFieldName = "actionBody"; 229 | CodeBlock headerBlock = null; 230 | if (TypeUtils.equalType(element, byte[].class)) { 231 | codeBlock.addStatement("$T $L = new $T($S, action.$L)", 232 | ActionBody.class, actionBodyFieldName, BytesArrayBody.class, encode, accessibleFieldName); 233 | } else if (TypeUtils.equalType(element, String.class)) { 234 | codeBlock.addStatement("$T $L = new $T($S, action.$L.getBytes())", 235 | ActionBody.class, actionBodyFieldName, BytesArrayBody.class, encode, accessibleFieldName); 236 | } else if (TypeUtils.equalType(element, File.class)) { 237 | codeBlock.addStatement("$T $L = new $T($S, action.$L)", ActionBody.class, actionBodyFieldName, FileBody.class, encode, accessibleFieldName); 238 | headerBlock = CodeBlock.builder() 239 | .add(".addHeader($S, action.$L.getName())", "filename", accessibleFieldName) 240 | .build(); 241 | } else if (TypeUtils.equalType(element, FileBody.class)) { 242 | codeBlock.addStatement("$T $L = action.$L", ActionBody.class, actionBodyFieldName, accessibleFieldName); 243 | headerBlock = CodeBlock.builder() 244 | .add(".addHeader($S, action.$L.getFile().getName())", "filename", accessibleFieldName) 245 | .build(); 246 | } else { 247 | codeBlock.addStatement("$T $L = action.$L", ActionBody.class, actionBodyFieldName, accessibleFieldName); 248 | } 249 | codeBlock.add("$["); 250 | codeBlock.add("$T $L = new $T().setBody($L)", MultipartRequestBody.PartBody.class, bodyFieldName, MultipartRequestBody.PartBody.Builder.class, actionBodyFieldName); 251 | if (headerBlock != null) codeBlock.add(headerBlock); 252 | codeBlock.add(".build()"); 253 | codeBlock.add(";\n$]"); 254 | } 255 | codeBlock.addStatement("requestBuilder.addPart($S, $S, $L)", partName, encode, bodyFieldName); 256 | builder.addCode(wrapFieldNotNull(codeBlock.build(), accessibleFieldName)); 257 | } 258 | } 259 | 260 | private MethodSpec createOnResponseMethod(HttpActionClass actionClass) { 261 | MethodSpec.Builder builder = MethodSpec.methodBuilder("onResponse") 262 | .addModifiers(Modifier.PUBLIC) 263 | .addAnnotation(Override.class) 264 | .returns(ClassName.get(actionClass.getTypeElement().asType())) 265 | .addParameter(actionClass.getTypeName(), "action") 266 | .addParameter(io.techery.janet.http.model.Response.class, "response") 267 | .addParameter(Converter.class, "converter") 268 | .addException(ConverterException.class); 269 | if (actionClass.getParent() != null) { 270 | builder.addStatement("$L.onResponse(action, response, converter)", PARENT_HELPER_FIELD_NAME); 271 | } 272 | addStatusField(actionClass, builder); 273 | addResponses(actionClass, builder); 274 | addResponseHeaders(actionClass, builder); 275 | builder.addStatement("return action"); 276 | return builder.build(); 277 | } 278 | 279 | private void addResponses(HttpActionClass actionClass, MethodSpec.Builder builder) { 280 | List responseElements = actionClass.getAnnotatedElements(Response.class); 281 | for (Element element : responseElements) { 282 | Response annotation 283 | = element.getAnnotation(Response.class); 284 | if (annotation.value() > 0) { 285 | builder.beginControlFlow("if (response.getStatus() == $L)", annotation.value()); 286 | addResponseStatements(actionClass, builder, element); 287 | builder.endControlFlow(); 288 | } else if (annotation.min() > 0 || annotation.max() > 0) { 289 | StringBuilder controlFlow = new StringBuilder("if("); 290 | if (annotation.min() > 0) { 291 | controlFlow.append("response.getStatus() >= ").append(annotation.min()); 292 | if (annotation.max() > 0) { 293 | controlFlow.append(" && "); 294 | } 295 | } 296 | if (annotation.max() > 0) { 297 | controlFlow.append("response.getStatus() <= ").append(annotation.max()); 298 | } 299 | controlFlow.append(")"); 300 | builder.beginControlFlow(controlFlow.toString()); 301 | addResponseStatements(actionClass, builder, element); 302 | builder.endControlFlow(); 303 | } else if (annotation.value() == Response.ERROR) { 304 | builder.beginControlFlow("if (!response.isSuccessful())"); 305 | addResponseStatements(actionClass, builder, element); 306 | builder.endControlFlow(); 307 | } else { 308 | builder.beginControlFlow("if (response.isSuccessful())"); 309 | addResponseStatements(actionClass, builder, element); 310 | builder.endControlFlow(); 311 | } 312 | } 313 | } 314 | 315 | private void addResponseStatements(HttpActionClass actionClass, MethodSpec.Builder builder, Element element) { 316 | String fieldAddress = getFieldAddress(actionClass, element); 317 | if (TypeUtils.equalType(element, ActionBody.class)) { 318 | builder.addStatement(fieldAddress + resolver.resolveAccessibleFieldNameToWrite(actionClass.getTypeElement(), element, "response.getBody()")); 319 | } else if (TypeUtils.equalType(element, String.class)) { 320 | builder 321 | .beginControlFlow("try") 322 | .addStatement(fieldAddress + resolver.resolveAccessibleFieldNameToWrite(actionClass.getTypeElement(), element, "$T.convertToString(response.getBody().getContent())"), StreamUtil.class) 323 | .nextControlFlow("catch($T e)", IOException.class) 324 | .addStatement("throw $T.forDeserialization(e)", ConverterException.class) 325 | .endControlFlow(); 326 | } else { 327 | builder.addStatement(fieldAddress + resolver.resolveAccessibleFieldNameToWrite(actionClass.getTypeElement(), element, "($T) converter.fromBody(response.getBody(), new $T<$T>(){}.getType())"), 328 | element.asType(), TypeToken.class, element.asType()); 329 | } 330 | } 331 | 332 | private void addResponseHeaders(HttpActionClass actionClass, MethodSpec.Builder builder) { 333 | if (actionClass.getAnnotatedElements(ResponseHeader.class).isEmpty()) { 334 | return; 335 | } 336 | builder.beginControlFlow("for ($T header : response.getHeaders())", Header.class); 337 | for (Element element : actionClass.getAnnotatedElements(ResponseHeader.class)) { 338 | ResponseHeader annotation = element.getAnnotation(ResponseHeader.class); 339 | String fieldAddress = getFieldAddress(actionClass, element); 340 | builder.beginControlFlow("if ($S.equals(header.getName()))", annotation.value()); 341 | builder.addStatement(fieldAddress + resolver.resolveAccessibleFieldNameToWrite(actionClass.getTypeElement(), element, "header.getValue()")); 342 | builder.endControlFlow(); 343 | } 344 | builder.endControlFlow(); 345 | 346 | } 347 | 348 | private void addStatusField(HttpActionClass actionClass, MethodSpec.Builder builder) { 349 | for (Element element : actionClass.getAnnotatedElements(Status.class)) { 350 | String fieldAddress = getFieldAddress(actionClass, element); 351 | if (TypeUtils.containsType(element, Boolean.class, boolean.class)) { 352 | builder.addStatement(fieldAddress + resolver.resolveAccessibleFieldNameToWrite(actionClass.getTypeElement(), element, "response.isSuccessful()")); 353 | } else if (TypeUtils.containsType(element, Integer.class, int.class, long.class)) { 354 | builder.addStatement(fieldAddress + resolver.resolveAccessibleFieldNameToWrite(actionClass.getTypeElement(), element, "($T) response.getStatus()"), element.asType()); 355 | } else if (TypeUtils.equalType(element, String.class)) { 356 | builder.addStatement(fieldAddress + resolver.resolveAccessibleFieldNameToWrite(actionClass.getTypeElement(), element, "Integer.toString(response.getStatus())")); 357 | } else if (TypeUtils.containsType(element, Long.class)) { 358 | builder.addStatement(fieldAddress + resolver.resolveAccessibleFieldNameToWrite(actionClass.getTypeElement(), element, "(long) response.getStatus()")); 359 | } 360 | } 361 | } 362 | 363 | private static String getFieldAddress(HttpActionClass actionClass, Element element) { 364 | String address; 365 | if (actionClass.getTypeElement().equals(element.getEnclosingElement())) { 366 | address = "action."; 367 | } else { 368 | address = String.format("((%s)action).", element.getEnclosingElement()); 369 | } 370 | return address; 371 | } 372 | 373 | private static CodeBlock wrapFieldNotNull(CodeBlock code, String fieldName) { 374 | return CodeBlock.builder() 375 | .beginControlFlow("if (action.$L != null)", fieldName) 376 | .add(code) 377 | .endControlFlow() 378 | .build(); 379 | } 380 | 381 | private static Iterable getTypeVariables(TypeElement element) { 382 | List typeVariables = new ArrayList(); 383 | for (TypeParameterElement type : element.getTypeParameters()) { 384 | typeVariables.add((TypeVariableName) TypeVariableName.get(type.asType())); 385 | } 386 | return typeVariables; 387 | } 388 | 389 | private final ElementResolver resolver; 390 | } 391 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/JanetHttpProcessor.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet; 2 | 3 | import com.google.auto.service.AutoService; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collection; 7 | import java.util.Collections; 8 | import java.util.List; 9 | import java.util.Set; 10 | 11 | import javax.annotation.processing.AbstractProcessor; 12 | import javax.annotation.processing.Filer; 13 | import javax.annotation.processing.Messager; 14 | import javax.annotation.processing.ProcessingEnvironment; 15 | import javax.annotation.processing.Processor; 16 | import javax.annotation.processing.RoundEnvironment; 17 | import javax.lang.model.SourceVersion; 18 | import javax.lang.model.element.Element; 19 | import javax.lang.model.element.PackageElement; 20 | import javax.lang.model.element.TypeElement; 21 | import javax.lang.model.type.TypeMirror; 22 | import javax.lang.model.util.Elements; 23 | import javax.lang.model.util.Types; 24 | import javax.tools.Diagnostic; 25 | 26 | import io.techery.janet.HttpActionService.ActionHelperFactory; 27 | import io.techery.janet.compiler.utils.validation.ClassValidator; 28 | import io.techery.janet.compiler.utils.validation.ValidationError; 29 | import io.techery.janet.http.annotations.HttpAction; 30 | import io.techery.janet.validation.HttpActionValidators; 31 | import io.techery.janet.util.ElementResolver; 32 | 33 | import static io.techery.janet.HttpActionService.HELPERS_FACTORY_CLASS_PACKAGE; 34 | import static io.techery.janet.HttpActionService.HELPERS_FACTORY_CLASS_SIMPLE_NAME; 35 | 36 | /** 37 | * Generates {@link HttpAction} helper classes and {@link ActionHelperFactory} implementation. 38 | *

39 | * Action helper is guaranteed to be generated only if it's not already in class path. 40 | *

41 | * Factory takes other factories into account, that could exists in class path (e.g. via external dependency) 42 | * to generate canonical factory that's loaded from {@link HttpActionService} on runtime. 43 | * If another canonical factory exists, it's ignored and possibly would cause clash on runtime. 44 | *

45 | * To generate unique factory (and could be used as dependency later), consider using annotation param {@link Options#OPTION_FACTORY_CLASS_SUFFIX} 46 | */ 47 | @AutoService(Processor.class) 48 | public class JanetHttpProcessor extends AbstractProcessor { 49 | private Elements elementUtils; 50 | private Messager messager; 51 | private ClassValidator classValidator; 52 | private HttpActionValidators httpActionValidators; 53 | private HttpHelpersFactoryGenerator httpHelpersFactoryGenerator; 54 | private HttpHelpersGenerator httpHelpersGenerator; 55 | private Types typesUtil; 56 | private Options options; 57 | 58 | private boolean processed; 59 | 60 | @Override 61 | public synchronized void init(ProcessingEnvironment processingEnv) { 62 | super.init(processingEnv); 63 | options = new Options(processingEnv.getOptions()); 64 | elementUtils = processingEnv.getElementUtils(); 65 | messager = processingEnv.getMessager(); 66 | typesUtil = processingEnv.getTypeUtils(); 67 | ElementResolver resolver = new ElementResolver(elementUtils); 68 | classValidator = new ClassValidator(HttpAction.class); 69 | httpActionValidators = new HttpActionValidators(resolver); 70 | Filer filer = processingEnv.getFiler(); 71 | 72 | httpHelpersFactoryGenerator = new HttpHelpersFactoryGenerator(filer, findOtherHelpersFactories(), options); 73 | httpHelpersGenerator = new HttpHelpersGenerator(filer, resolver); 74 | } 75 | 76 | @Override 77 | public Set getSupportedAnnotationTypes() { 78 | return Collections.singleton("*"); 79 | } 80 | 81 | @Override 82 | public Set getSupportedOptions() { 83 | return Collections.singleton(Options.OPTION_FACTORY_CLASS_SUFFIX); 84 | } 85 | 86 | @Override 87 | public SourceVersion getSupportedSourceVersion() { 88 | return SourceVersion.latestSupported(); 89 | } 90 | 91 | @Override 92 | public boolean process(Set annotations, RoundEnvironment roundEnv) { 93 | if (processed) return false; 94 | else processed = true; 95 | // 96 | ArrayList actionClasses = new ArrayList(); 97 | for (Element actionElement : roundEnv.getElementsAnnotatedWith(HttpAction.class)) { 98 | TypeElement typeElement = (TypeElement) actionElement; 99 | HttpActionClass actionClass = createActionClass(typeElement); 100 | if (actionClass != null) { 101 | Set errors = httpActionValidators.validate(actionClass); 102 | if (!errors.isEmpty()) { 103 | printErrors(errors); 104 | } 105 | actionClasses.add(actionClass); 106 | } 107 | } 108 | httpHelpersGenerator.generate(actionClasses); 109 | httpHelpersFactoryGenerator.generate(actionClasses); 110 | // 111 | return false; 112 | } 113 | 114 | private List findOtherHelpersFactories() { 115 | PackageElement packageElement = elementUtils.getPackageElement(HELPERS_FACTORY_CLASS_PACKAGE); 116 | if (packageElement == null) return Collections.emptyList(); 117 | // 118 | List factoryElements = new ArrayList(); 119 | TypeElement interfaceElement = elementUtils.getTypeElement(ActionHelperFactory.class.getCanonicalName()); 120 | for (Element element : packageElement.getEnclosedElements()) { 121 | if (element.equals(interfaceElement) 122 | || element.getSimpleName().contentEquals(HELPERS_FACTORY_CLASS_SIMPLE_NAME)) continue; 123 | // 124 | for (TypeMirror typeMirror : ((TypeElement) element).getInterfaces()) { 125 | if (typesUtil.isAssignable(typeMirror, interfaceElement.asType())) { 126 | factoryElements.add(((TypeElement) element)); 127 | break; 128 | } 129 | } 130 | } 131 | return factoryElements; 132 | } 133 | 134 | private HttpActionClass createActionClass(TypeElement actionElement) { 135 | Set errors = classValidator.validate(actionElement); 136 | if (!errors.isEmpty()) { 137 | printErrors(errors); 138 | return null; 139 | } 140 | HttpActionClass parent = null; 141 | if (actionElement.getSuperclass() != null) { 142 | TypeElement parentElement = (TypeElement) typesUtil.asElement(actionElement.getSuperclass()); 143 | if (parentElement != null) { 144 | HttpActionClass subClass = createActionClass(parentElement); 145 | if (subClass != null && !subClass.getAllAnnotatedMembers().isEmpty()) { 146 | parent = subClass; 147 | } 148 | } 149 | } 150 | return new HttpActionClass(elementUtils, actionElement, parent); 151 | } 152 | 153 | private void printErrors(Collection errors) { 154 | for (ValidationError error : errors) { 155 | messager.printMessage(Diagnostic.Kind.ERROR, error.getMessage(), error.getElement()); 156 | } 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/Options.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet; 2 | 3 | import java.util.Map; 4 | 5 | public class Options { 6 | 7 | public static final String OPTION_FACTORY_CLASS_SUFFIX = "janet.http.factory.class.suffix"; 8 | public final String factoryClassSuffix; 9 | 10 | public Options(Map options) { 11 | this.factoryClassSuffix = options.get(OPTION_FACTORY_CLASS_SUFFIX); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/util/ElementResolver.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.util; 2 | 3 | import java.lang.annotation.Annotation; 4 | import java.util.HashSet; 5 | import java.util.Locale; 6 | import java.util.Set; 7 | import java.util.regex.Pattern; 8 | 9 | import javax.lang.model.element.AnnotationMirror; 10 | import javax.lang.model.element.Element; 11 | import javax.lang.model.element.ElementKind; 12 | import javax.lang.model.element.TypeElement; 13 | import javax.lang.model.type.DeclaredType; 14 | import javax.lang.model.type.NoType; 15 | import javax.lang.model.type.TypeMirror; 16 | import javax.lang.model.util.Elements; 17 | 18 | 19 | public class ElementResolver { 20 | 21 | private final Elements elementUtils; 22 | 23 | public ElementResolver(Elements elementUtils) { 24 | this.elementUtils = elementUtils; 25 | } 26 | 27 | public boolean isKotlinClass(TypeElement typeElement) { 28 | // simpler check is possible (getAnnotation(kotlin.Metadata.class) != null) but we do not want to add kotlin dependency 29 | boolean isKotlinClass = false; 30 | for (AnnotationMirror annotationMirror : elementUtils.getAllAnnotationMirrors(typeElement)) { 31 | if (((TypeElement) annotationMirror.getAnnotationType().asElement()).getQualifiedName() 32 | .toString() 33 | .equals("kotlin.Metadata")) 34 | isKotlinClass = true; 35 | } 36 | 37 | return isKotlinClass; 38 | } 39 | 40 | public boolean checkHasAnnotatedField(Class annotationClass, TypeElement typeElement) { 41 | boolean hasAnnotatedField = false; 42 | 43 | for (Element element : elementUtils.getAllMembers(typeElement)) { 44 | if (element.getKind() == ElementKind.FIELD) { 45 | if (element.getAnnotation(annotationClass) != null) { 46 | hasAnnotatedField = true; 47 | } 48 | } 49 | } 50 | 51 | return hasAnnotatedField; 52 | } 53 | 54 | public Set getAnnotatedFieldNames(Class annotationClass, TypeElement typeElement) { 55 | Set names = new HashSet(); 56 | for (Element element : elementUtils.getAllMembers(typeElement)) { 57 | if (element.getKind() == ElementKind.FIELD) { 58 | if (element.getAnnotation(annotationClass) != null) { 59 | names.add(resolveAccessibleFieldNameToRead(typeElement, element)); 60 | } 61 | } 62 | } 63 | 64 | return names; 65 | } 66 | 67 | public boolean hasAccessorByField(TypeElement typeElement, Element element) { 68 | if (element.getKind() != ElementKind.FIELD) throw new IllegalArgumentException("Element must be field"); 69 | if (!isKotlinClass(typeElement)) return true; 70 | String getterByFieldName = String.format(Locale.US, "get%s", capitalize(element.getSimpleName().toString())); 71 | for (Element e : elementUtils.getAllMembers(typeElement)) { 72 | if (e.getKind() != ElementKind.METHOD) continue; 73 | if (e.getSimpleName().toString().equals(getterByFieldName)) return true; 74 | } 75 | return false; 76 | } 77 | 78 | public String resolveAccessibleFieldNameToRead(TypeElement typeElement, Element element) { 79 | if (element.getKind() != ElementKind.FIELD) throw new IllegalArgumentException("Element must be field"); 80 | final String fieldName = element.getSimpleName().toString(); 81 | if (isKotlinClass(typeElement)) { 82 | if (Pattern.compile("^is[A-Z]+").matcher(fieldName).matches()) { 83 | return fieldName + "()"; 84 | } else { 85 | Element methodElement = null; 86 | String methodByFieldName = String.format(Locale.US, "get%s", capitalize(fieldName)); 87 | for (Element e : elementUtils.getAllMembers(typeElement)) { 88 | if (e.getKind() == ElementKind.METHOD && e.getSimpleName().toString() 89 | .startsWith(methodByFieldName)) { 90 | methodElement = e; 91 | break; 92 | } 93 | } 94 | return methodElement.getSimpleName().toString() + "()"; 95 | } 96 | } else { 97 | return fieldName; 98 | } 99 | } 100 | 101 | public String resolveAccessibleFieldNameToWrite(TypeElement typeElement, Element element, String value) { 102 | if (element.getKind() != ElementKind.FIELD) throw new IllegalArgumentException("Element must be field"); 103 | final String fieldName = element.getSimpleName().toString(); 104 | if (isKotlinClass(typeElement)) { 105 | Element methodElement = null; 106 | String methodByFieldName = String.format(Locale.US, "set%s", capitalize(fieldName)); 107 | for (Element e : elementUtils.getAllMembers(typeElement)) { 108 | if (e.getKind() == ElementKind.METHOD && e.getSimpleName().toString() 109 | .startsWith(methodByFieldName)) { 110 | methodElement = e; 111 | break; 112 | } 113 | } 114 | return String.format(Locale.US, "%s(%s)", methodElement.getSimpleName() 115 | .toString() 116 | .replace("$", "$$"), value); 117 | } else { 118 | return fieldName + " = " + value; 119 | } 120 | } 121 | 122 | public TypeElement getSuperclass(TypeElement typeElement) { 123 | TypeMirror superTypeMirror = typeElement.getSuperclass(); 124 | 125 | if (superTypeMirror instanceof NoType) { 126 | return null; 127 | } 128 | 129 | TypeElement superTypeElement = 130 | (TypeElement) ((DeclaredType) superTypeMirror).asElement(); 131 | 132 | if (superTypeElement.getQualifiedName().toString().equals(Object.class.getCanonicalName())) { 133 | return null; 134 | } 135 | 136 | return superTypeElement; 137 | } 138 | 139 | public static String capitalize(String value) { 140 | return value.substring(0, 1).toUpperCase(Locale.US) + value.substring(1); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/AnnotationQuantityValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import java.util.HashSet; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import javax.lang.model.element.Element; 8 | 9 | import io.techery.janet.HttpActionClass; 10 | import io.techery.janet.compiler.utils.validation.ValidationError; 11 | import io.techery.janet.compiler.utils.validation.Validator; 12 | 13 | public class AnnotationQuantityValidator implements Validator { 14 | 15 | private final Class annotationClass; 16 | private final int maxQuantity; 17 | 18 | public AnnotationQuantityValidator(Class annotationClass, int maxQuantity) { 19 | this.annotationClass = annotationClass; 20 | this.maxQuantity = maxQuantity; 21 | } 22 | 23 | @Override 24 | public Set validate(HttpActionClass value) { 25 | Set errors = new HashSet(); 26 | List annotations = value.getAllAnnotatedElements(annotationClass); 27 | if (annotations.size() > maxQuantity) { 28 | errors.add(new ValidationError("There are more then one field annotated with %s", annotations.get(maxQuantity), annotationClass 29 | .getName())); 30 | } 31 | return errors; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/AnnotationTypesValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import com.squareup.javapoet.TypeName; 4 | 5 | import java.lang.annotation.Annotation; 6 | import java.lang.reflect.Type; 7 | import java.util.ArrayList; 8 | import java.util.HashSet; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | import javax.lang.model.element.Element; 13 | 14 | import io.techery.janet.HttpActionClass; 15 | import io.techery.janet.compiler.utils.TypeUtils; 16 | import io.techery.janet.compiler.utils.validation.ValidationError; 17 | import io.techery.janet.compiler.utils.validation.Validator; 18 | 19 | public class AnnotationTypesValidator implements Validator { 20 | 21 | private final Class annotationClass; 22 | private Type[] types = new Type[0]; 23 | private TypeName[] typeNames = new TypeName[0]; 24 | private final ArrayList typeStrings; 25 | 26 | public AnnotationTypesValidator(Class annotationClass, Type... types) { 27 | this(annotationClass, types, new TypeName[]{}); 28 | } 29 | 30 | public AnnotationTypesValidator(Class annotationClass, TypeName... typeNames) { 31 | this(annotationClass, new Type[]{}, typeNames); 32 | } 33 | 34 | public AnnotationTypesValidator(Class annotationClass, Type[] types, TypeName[] typeNames) { 35 | this.annotationClass = annotationClass; 36 | this.types = types; 37 | this.typeNames = typeNames; 38 | typeStrings = new ArrayList(); 39 | for (Type type : types) { 40 | typeStrings.add(TypeName.get(type).toString()); 41 | } 42 | for (TypeName type : typeNames) { 43 | typeStrings.add(type.toString()); 44 | } 45 | } 46 | 47 | @Override 48 | public Set validate(HttpActionClass value) { 49 | Set errors = new HashSet(); 50 | List elements = value.getAllAnnotatedElements(annotationClass); 51 | for (Element element : elements) { 52 | Annotation annotation = element.getAnnotation(annotationClass); 53 | if (TypeUtils.containsType(element, types)) { 54 | continue; 55 | } 56 | if (TypeUtils.containsType(element, typeNames)) { 57 | continue; 58 | } 59 | errors.add(new ValidationError("Fields annotated with %s should one from these types %s", element, annotation 60 | .toString(), typeStrings.toString())); 61 | } 62 | return errors; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/BodyValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | import javax.lang.model.element.Element; 9 | 10 | import io.techery.janet.HttpActionClass; 11 | import io.techery.janet.compiler.utils.validation.ValidationError; 12 | import io.techery.janet.compiler.utils.validation.Validator; 13 | import io.techery.janet.http.annotations.Body; 14 | import io.techery.janet.http.annotations.HttpAction; 15 | 16 | public class BodyValidator implements Validator { 17 | @Override 18 | public Set validate(HttpActionClass value) { 19 | Set errors = new HashSet(); 20 | List annotations = value.getAllAnnotatedElements(Body.class); 21 | if (annotations.isEmpty()) return errors; 22 | Element element = annotations.get(0); 23 | 24 | if (value.getMethod().hasBody()) return errors; 25 | 26 | List methodNames = new ArrayList(); 27 | for (HttpAction.Method method : HttpAction.Method.values()) { 28 | if (!method.hasBody()) continue; 29 | methodNames.add(method.name()); 30 | } 31 | errors.add(new ValidationError(String.format("It's possible to use %s only with %s methods ", element, Body.class 32 | .getName(), methodNames.toString()), value.getTypeElement())); 33 | return errors; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/FieldsModifiersValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | 6 | import javax.lang.model.element.Element; 7 | import javax.lang.model.element.ElementKind; 8 | import javax.lang.model.element.Modifier; 9 | import javax.lang.model.element.TypeElement; 10 | 11 | import io.techery.janet.compiler.utils.ActionClass; 12 | import io.techery.janet.compiler.utils.validation.ValidationError; 13 | import io.techery.janet.compiler.utils.validation.Validator; 14 | import io.techery.janet.util.ElementResolver; 15 | 16 | class FieldsModifiersValidator implements Validator { 17 | 18 | private final ElementResolver resolver; 19 | 20 | FieldsModifiersValidator(ElementResolver resolver) {this.resolver = resolver;} 21 | 22 | @Override 23 | public Set validate(T value) { 24 | Set messages = new HashSet(); 25 | for (Element element : value.getAllAnnotatedMembers()) { 26 | if (element.getKind() != ElementKind.FIELD) continue; 27 | boolean hasPrivateModifier = element.getModifiers().contains(Modifier.PRIVATE); 28 | boolean hasStaticModifier = element.getModifiers().contains(Modifier.STATIC); 29 | if (resolver.isKotlinClass(value.getTypeElement())) { 30 | if (!isKotlinValid(value.getTypeElement(), element)) { 31 | messages.add(new ValidationError("Annotated fields must have public accessors", element)); 32 | } 33 | } else if (hasStaticModifier || hasPrivateModifier) { 34 | messages.add(new ValidationError("Annotated fields can't be static or private", element)); 35 | } 36 | } 37 | return messages; 38 | } 39 | 40 | private boolean isKotlinValid(TypeElement classElement, Element element) { 41 | return resolver.hasAccessorByField(classElement, element); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/HttpActionValidators.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import com.squareup.javapoet.ClassName; 4 | import com.squareup.javapoet.TypeName; 5 | 6 | import java.io.File; 7 | import java.lang.reflect.Type; 8 | import java.net.URI; 9 | import java.util.ArrayList; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Set; 13 | 14 | import io.techery.janet.HttpActionClass; 15 | import io.techery.janet.body.ActionBody; 16 | import io.techery.janet.body.BytesArrayBody; 17 | import io.techery.janet.body.FileBody; 18 | import io.techery.janet.compiler.utils.validation.ValidationError; 19 | import io.techery.janet.compiler.utils.validation.Validator; 20 | import io.techery.janet.http.annotations.Body; 21 | import io.techery.janet.http.annotations.Field; 22 | import io.techery.janet.http.annotations.HttpAction; 23 | import io.techery.janet.http.annotations.Part; 24 | import io.techery.janet.http.annotations.ResponseHeader; 25 | import io.techery.janet.http.annotations.Status; 26 | import io.techery.janet.http.annotations.Url; 27 | import io.techery.janet.http.model.FormUrlEncodedRequestBody; 28 | import io.techery.janet.http.model.MultipartRequestBody; 29 | import io.techery.janet.util.ElementResolver; 30 | 31 | public class HttpActionValidators implements Validator { 32 | 33 | private final List> validators; 34 | 35 | public HttpActionValidators(ElementResolver resolver) { 36 | validators = new ArrayList>(); 37 | //general rules 38 | validators.add(new FieldsModifiersValidator(resolver)); 39 | validators.add(new ParentsValidator()); 40 | validators.add(new PathValidator()); 41 | validators.add(new BodyValidator()); 42 | validators.add(new RequestTypeValidator(Body.class, HttpAction.Type.SIMPLE)); 43 | validators.add(new RequestTypeValidator(Field.class, HttpAction.Type.FORM_URL_ENCODED)); 44 | validators.add(new RequestTypeValidator(Part.class, HttpAction.Type.MULTIPART)); 45 | validators.add(new ResponseValidator()); 46 | validators.add(new UrlValidator()); 47 | //annotation rules 48 | validators.add(new AnnotationQuantityValidator(Body.class, 1)); 49 | validators.add(new AnnotationQuantityValidator(Url.class, 1)); 50 | validators.add(new AnnotationTypesValidator(ResponseHeader.class, String.class)); 51 | validators.add(new AnnotationTypesValidator(Status.class, Boolean.class, Integer.class, Long.class, String.class, boolean.class, int.class, long.class)); 52 | validators.add(new AnnotationTypesValidator(Part.class, MultipartRequestBody.PartBody.class, File.class, byte[].class, String.class, ActionBody.class, 53 | BytesArrayBody.class, MultipartRequestBody.class, FormUrlEncodedRequestBody.class, FileBody.class)); 54 | validators.add(new AnnotationTypesValidator(Url.class, new Type[]{String.class, URI.class}, 55 | new TypeName[]{ClassName.get("android.net", "Uri"), ClassName.get("okhttp3", "HttpUrl"), ClassName.get("com.squareup.okhttp", "HttpUrl")})); 56 | } 57 | 58 | @Override 59 | public Set validate(HttpActionClass value) { 60 | Set errors = new HashSet(); 61 | for (Validator validator : validators) { 62 | errors.addAll(validator.validate(value)); 63 | } 64 | return errors; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/ParentsValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import java.util.Collections; 4 | import java.util.Set; 5 | 6 | import io.techery.janet.HttpActionClass; 7 | import io.techery.janet.compiler.utils.validation.ValidationError; 8 | import io.techery.janet.compiler.utils.validation.Validator; 9 | 10 | public class ParentsValidator implements Validator { 11 | @Override public Set validate(HttpActionClass value) { 12 | while (value.getParent() != null) { 13 | value = value.getParent(); 14 | if (value.isAnnotatedClass()) { 15 | return Collections.singleton(new ValidationError("Parent class cant't be annotated with @HttpAction", value 16 | .getTypeElement())); 17 | } 18 | } 19 | return Collections.emptySet(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/PathValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import org.apache.commons.lang.StringUtils; 4 | 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | import javax.lang.model.element.Element; 12 | import javax.lang.model.element.TypeElement; 13 | 14 | import io.techery.janet.HttpActionClass; 15 | import io.techery.janet.compiler.utils.validation.ValidationError; 16 | import io.techery.janet.compiler.utils.validation.Validator; 17 | import io.techery.janet.http.annotations.HttpAction; 18 | import io.techery.janet.http.annotations.Path; 19 | import io.techery.janet.http.annotations.Url; 20 | 21 | public class PathValidator implements Validator { 22 | 23 | private static final String PATH_FORMAT_DEFINITION = "{%s}"; 24 | private static final Pattern PATH_PATTERN = Pattern.compile("[{](.*?)[}]"); 25 | 26 | @Override 27 | public Set validate(HttpActionClass value) { 28 | Set errors = new HashSet(); 29 | TypeElement baseElement = value.getTypeElement(); 30 | List pathAnnotations = value.getAllAnnotatedElements(Path.class); 31 | 32 | if (value.getAllAnnotatedElements(Url.class).isEmpty()) { 33 | if (StringUtils.isEmpty(value.getPath()) && value.getAllAnnotatedElements(Url.class).isEmpty()) { 34 | errors.add(new ValidationError(String.format("Path in @%s for class %s is null or empty! That's not allowed", baseElement, 35 | HttpAction.class.getSimpleName(), baseElement.getQualifiedName().toString()), baseElement)); 36 | } 37 | 38 | //Validate that annotated with Path variables exists in path of HttpAction 39 | for (Element element : pathAnnotations) { 40 | Path annotation = element.getAnnotation(Path.class); 41 | String formatedPath = String.format(PATH_FORMAT_DEFINITION, annotation.value()); 42 | if (value.getPath().contains(formatedPath)) continue; 43 | errors.add(new ValidationError(String.format("%s annotated variable doesn't exist in your path", element, Path.class 44 | .getName()), baseElement)); 45 | } 46 | 47 | //Validate that specified variable in path, has specified right annotated variable in class 48 | if (value.isAnnotatedClass()) { 49 | Matcher matcher = PATH_PATTERN.matcher(value.getPath()); 50 | while (matcher.find()) { 51 | boolean hasAnnotatedVariable = false; 52 | String group = matcher.group(1); 53 | for (Element element : pathAnnotations) { 54 | Path annotation = element.getAnnotation(Path.class); 55 | if (annotation.value().equals(group)) { 56 | hasAnnotatedVariable = true; 57 | break; 58 | } 59 | } 60 | if (!hasAnnotatedVariable) { 61 | errors.add(new ValidationError(String.format("Annotate variable with %s annotation with value \"%s\"", Path.class 62 | .getName(), group), baseElement)); 63 | } 64 | } 65 | } 66 | } 67 | return errors; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/RequestTypeValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashSet; 5 | import java.util.List; 6 | import java.util.Set; 7 | 8 | import javax.lang.model.element.Element; 9 | 10 | import io.techery.janet.HttpActionClass; 11 | import io.techery.janet.compiler.utils.validation.ValidationError; 12 | import io.techery.janet.compiler.utils.validation.Validator; 13 | import io.techery.janet.http.annotations.HttpAction; 14 | 15 | public class RequestTypeValidator implements Validator { 16 | 17 | private final Class annotationClass; 18 | private final HttpAction.Type[] requestTypes; 19 | 20 | public RequestTypeValidator(Class annotationClass, HttpAction.Type... requestTypes) { 21 | this.annotationClass = annotationClass; 22 | this.requestTypes = requestTypes; 23 | } 24 | 25 | @Override 26 | public Set validate(HttpActionClass value) { 27 | Set errors = new HashSet(); 28 | String bodyName = annotationClass.getSimpleName(); 29 | List typesList = Arrays.asList(requestTypes); 30 | for (Element element : value.getAllAnnotatedElements(annotationClass)) { 31 | if (!typesList.contains(value.getRequestType())) { 32 | errors.add(new ValidationError("It's possible to use %s only with %s request types ", element, bodyName, typesList 33 | .toString())); 34 | } 35 | } 36 | return errors; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/ResponseValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import java.util.HashSet; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import javax.lang.model.element.Element; 8 | 9 | import io.techery.janet.HttpActionClass; 10 | import io.techery.janet.compiler.utils.validation.ValidationError; 11 | import io.techery.janet.compiler.utils.validation.Validator; 12 | import io.techery.janet.http.annotations.Response; 13 | 14 | public class ResponseValidator implements Validator { 15 | @Override 16 | public Set validate(HttpActionClass value) { 17 | Set errors = new HashSet(); 18 | ValidationError error = validateInternal(value); 19 | if (error != null) { 20 | errors.add(error); 21 | } 22 | return errors; 23 | } 24 | 25 | private static ValidationError validateInternal(HttpActionClass value) { 26 | List annotations = value.getAllAnnotatedElements(Response.class); 27 | if (annotations.isEmpty()) return null; 28 | for (Element element : annotations) { 29 | Response annotation = element.getAnnotation(Response.class); 30 | if (annotation.value() < Response.ERROR) { 31 | return lessThanZero("@Response.value()", element); 32 | } 33 | if (annotation.max() < 0) { 34 | return lessThanZero("@Response.max()", element); 35 | } 36 | if (annotation.min() < 0) { 37 | return lessThanZero("@Response.min()", element); 38 | } 39 | if (annotation.value() != 0 40 | && (annotation.min() > 0 || annotation.max() > 0)) { 41 | return new ValidationError("There is no possibility to specify status code with using arguments min() and max()", element); 42 | } 43 | } 44 | return null; 45 | } 46 | 47 | private static ValidationError lessThanZero(String label, Element element) { 48 | return new ValidationError("%s can not be less than 0", element, label); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /service-compiler/src/main/java/io/techery/janet/validation/UrlValidator.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.validation; 2 | 3 | import org.apache.commons.lang.StringUtils; 4 | 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import java.util.Set; 8 | 9 | import javax.lang.model.element.Element; 10 | 11 | import io.techery.janet.HttpActionClass; 12 | import io.techery.janet.compiler.utils.validation.ValidationError; 13 | import io.techery.janet.compiler.utils.validation.Validator; 14 | import io.techery.janet.http.annotations.Url; 15 | 16 | public class UrlValidator implements Validator { 17 | @Override 18 | public Set validate(HttpActionClass value) { 19 | Set errors = new HashSet(); 20 | ValidationError error = validateInternal(value); 21 | if (error != null) { 22 | errors.add(error); 23 | } 24 | return errors; 25 | } 26 | 27 | private static ValidationError validateInternal(HttpActionClass value) { 28 | List annotatedElements = value.getAllAnnotatedElements(Url.class); 29 | if (!annotatedElements.isEmpty() && !StringUtils.isEmpty(value.getPath())) { 30 | return new ValidationError("@Url can't be used with specified path (@HttpAction.value())", annotatedElements 31 | .get(0)); 32 | } 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /service/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'idea' 3 | apply from: rootProject.file('gradle/plugins/maven-simple.gradle') 4 | 5 | compileJava { 6 | sourceCompatibility = '1.6' 7 | targetCompatibility = '1.6' 8 | } 9 | compileTestJava { 10 | sourceCompatibility = '1.8' 11 | } 12 | 13 | dependencies { 14 | compile 'com.github.techery:janet:1.0.7' 15 | compile 'com.github.techery.janet-converters:base-body:1.1.1' 16 | compile 'com.github.techery.janet-converters:base-converter:1.1.1' 17 | } 18 | -------------------------------------------------------------------------------- /service/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | ## Keep Generated HttpActionHelperFactory 2 | -keep class * implements io.techery.janet.HttpActionService$ActionHelperFactory 3 | # 4 | ## Annotation processor (compiler) classes should be ignored 5 | -dontwarn javax.servlet.** 6 | -dontwarn com.google.auto.common.** 7 | -dontwarn com.google.auto.service.processor.** 8 | -dontwarn com.squareup.javapoet.** 9 | -dontwarn org.apache.commons.collections.BeanMap 10 | -dontwarn org.apache.tools.** 11 | -dontwarn org.apache.velocity.** 12 | -dontwarn io.techery.janet.compiler.** 13 | -dontwarn io.techery.janet.validation.** 14 | -dontwarn io.techery.janet.JanetHttpProcessor 15 | -dontwarn io.techery.janet.HttpActionClass 16 | -dontwarn io.techery.janet.HttpHelpersGenerator 17 | -dontwarn io.techery.janet.HelpersFactoryGenerator 18 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/HttpActionService.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet; 2 | 3 | import java.net.MalformedURLException; 4 | import java.net.URL; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.CopyOnWriteArrayList; 10 | 11 | import io.techery.janet.converter.Converter; 12 | import io.techery.janet.converter.ConverterException; 13 | import io.techery.janet.http.HttpClient; 14 | import io.techery.janet.http.annotations.Body; 15 | import io.techery.janet.http.annotations.Field; 16 | import io.techery.janet.http.annotations.HttpAction; 17 | import io.techery.janet.http.annotations.Part; 18 | import io.techery.janet.http.annotations.Path; 19 | import io.techery.janet.http.annotations.Query; 20 | import io.techery.janet.http.annotations.RequestHeader; 21 | import io.techery.janet.http.annotations.ResponseHeader; 22 | import io.techery.janet.http.annotations.Status; 23 | import io.techery.janet.http.exception.HttpDeserializationException; 24 | import io.techery.janet.http.exception.HttpException; 25 | import io.techery.janet.http.exception.HttpSerializationException; 26 | import io.techery.janet.http.exception.HttpServiceException; 27 | import io.techery.janet.http.model.Request; 28 | import io.techery.janet.http.model.Response; 29 | 30 | /** 31 | * Provide HTTP/HTTPS requests execution. Each HTTP request for {@linkplain HttpActionService} is an individual class that contains 32 | * all information about the request and response. Http action must be annotated with {@linkplain HttpAction @HttpAction}. 33 | *

{@code  @HttpAction(value = "/demo", method = HttpAction.Method.GET)
 34 |  * public class ExampleAction {}
 35 |  * }
 36 |  * 
37 | * To configure request, Action fields can be annotated with: 38 | *
    39 | *
  • {@linkplain Path @Path} for path value
  • 40 | *
  • {@linkplain Query @Query} for request URL parameters
  • 41 | *
  • {@linkplain Body @Body} for POST request body
  • 42 | *
  • {@linkplain RequestHeader @RequestHeader} for request headers
  • 43 | *
  • {@linkplain Field @Field} for request fields if request type is {@linkplain HttpAction.Type#FORM_URL_ENCODED}
  • 44 | *
  • {@linkplain Part @Part} for multipart request parts
  • 45 | *
46 | * To process response, special annotations can be used: 47 | *
    48 | *
  • {@linkplain io.techery.janet.http.annotations.Response @Response} for getting response body
  • 49 | *
  • {@linkplain Status @Status} for getting response status. Field types Integer, Long, int or long can be used 50 | * to get status code or use boolean to know that request was sent successfully
  • 51 | *
  • {@linkplain ResponseHeader @ResponseHeader} for getting response headers
  • 52 | *
53 | */ 54 | final public class HttpActionService extends ActionService { 55 | 56 | final static String HELPERS_FACTORY_CLASS_SIMPLE_NAME = "HttpActionHelperFactory"; 57 | final static String HELPERS_FACTORY_CLASS_PACKAGE = HttpActionService.class.getPackage().getName() + ".http"; 58 | final static String HELPERS_FACTORY_CLASS_NAME = HELPERS_FACTORY_CLASS_PACKAGE + "." + HELPERS_FACTORY_CLASS_SIMPLE_NAME; 59 | 60 | private ActionHelperFactory actionHelperFactory; 61 | private final Map actionHelperCache; 62 | private final Map> runningRequests; 63 | 64 | private final HttpClient client; 65 | private final Converter converter; 66 | private final String baseUrl; 67 | 68 | private static final int PROGRESS_THRESHOLD = 5; 69 | 70 | public HttpActionService(String baseUrl, HttpClient client, Converter converter) { 71 | if (baseUrl == null) { 72 | throw new IllegalArgumentException("baseUrl == null"); 73 | } 74 | if (client == null) { 75 | throw new IllegalArgumentException("client == null"); 76 | } 77 | if (converter == null) { 78 | throw new IllegalArgumentException("converter == null"); 79 | } 80 | try { 81 | new URL(baseUrl); 82 | } catch (MalformedURLException t) { 83 | throw new IllegalArgumentException("baseUrl is not valid", t); 84 | } 85 | this.baseUrl = baseUrl; 86 | this.client = client; 87 | this.converter = converter; 88 | this.actionHelperCache = new HashMap(); 89 | this.runningRequests = new ConcurrentHashMap>(); 90 | loadActionHelperFactory(); 91 | } 92 | 93 | @Override protected Class getSupportedAnnotationType() { 94 | return HttpAction.class; 95 | } 96 | 97 | @Override protected
void sendInternal(ActionHolder holder) throws HttpServiceException { 98 | callback.onStart(holder); 99 | A action = holder.action(); 100 | final ActionHelper helper = getActionHelper(action.getClass()); 101 | if (helper == null) { 102 | throw new JanetInternalException("Something was happened with code generator. Check dependence of janet-http-compiler"); 103 | } 104 | putRunningAction(action); 105 | RequestBuilder builder = new RequestBuilder(baseUrl, converter); 106 | Response response = null; 107 | Request request = null; 108 | try { 109 | builder = helper.fillRequest(builder, action); 110 | request = builder.build(); 111 | putRunningRequest(action, request); 112 | throwIfCanceled(action, request); 113 | try { 114 | response = client.execute(request, new ActionRequestCallback(holder) { 115 | private int lastProgress; 116 | 117 | @Override public void onProgress(int progress) { 118 | if (progress > lastProgress + PROGRESS_THRESHOLD) { 119 | callback.onProgress(holder, progress); 120 | lastProgress = progress; 121 | } 122 | } 123 | }); 124 | } finally { 125 | throwIfCanceled(action, request); 126 | } 127 | action = helper.onResponse(action, response, converter); 128 | if (!response.isSuccessful()) { 129 | throw HttpException.forResponse(request, response); 130 | } 131 | throwIfCanceled(action, request); 132 | } catch (CancelException e) { 133 | return; 134 | } catch (ConverterException e) { 135 | Throwable cause; 136 | if (response != null) { 137 | cause = new HttpDeserializationException(e, response); 138 | } else { 139 | cause = new HttpSerializationException(e, request); 140 | } 141 | throw new HttpServiceException(cause); 142 | } catch (HttpException e) { 143 | throw new HttpServiceException(e); 144 | } catch (Throwable e) { // else is request related issue 145 | throw new HttpServiceException(HttpException.forRequest(request, e)); 146 | } finally { 147 | if (request != null) { 148 | List requests = runningRequests.get(action); 149 | if (requests != null) { 150 | requests.remove(request); 151 | } 152 | } else { 153 | runningRequests.remove(action); 154 | } 155 | } 156 | this.callback.onSuccess(holder); 157 | } 158 | 159 | @Override protected void cancel(ActionHolder holder) { 160 | A action = holder.action(); 161 | List requests = runningRequests.remove(action); 162 | try { 163 | if (requests != null) { 164 | for (Request request : requests) { 165 | client.cancel(request); 166 | } 167 | } 168 | } catch (Throwable ignored) {} 169 | } 170 | 171 | private void putRunningRequest(Object action, Request request) { 172 | putRunningAction(action); 173 | runningRequests.get(action).add(request); 174 | } 175 | 176 | private void putRunningAction(Object action) { 177 | if (!runningRequests.containsKey(action)) { 178 | runningRequests.put(action, new CopyOnWriteArrayList()); 179 | } 180 | } 181 | 182 | private void throwIfCanceled(Object action, Request request) throws CancelException { 183 | List requests = runningRequests.get(action); 184 | if (requests == null || !requests.contains(request)) { 185 | throw new CancelException(); 186 | } 187 | } 188 | 189 | private ActionHelper getActionHelper(Class actionClass) { 190 | ActionHelper helper = actionHelperCache.get(actionClass); 191 | if (helper == null && actionHelperFactory != null) { 192 | synchronized (actionHelperFactory) { 193 | helper = actionHelperFactory.make(actionClass); 194 | actionHelperCache.put(actionClass, helper); 195 | } 196 | } 197 | return helper; 198 | } 199 | 200 | private void loadActionHelperFactory() { 201 | try { 202 | Class clazz 203 | = (Class) Class.forName(HELPERS_FACTORY_CLASS_NAME); 204 | actionHelperFactory = clazz.newInstance(); 205 | } catch (Exception e) { 206 | throw new JanetInternalException("Can't initialize ActionHelperFactory - generator failed", e); 207 | } 208 | } 209 | 210 | public interface ActionHelperFactory { 211 | ActionHelper make(Class actionClass); 212 | } 213 | 214 | public interface ActionHelper { 215 | RequestBuilder fillRequest(RequestBuilder requestBuilder, T action) throws ConverterException; 216 | 217 | T onResponse(T action, Response response, Converter converter) throws ConverterException; 218 | } 219 | 220 | private static abstract class ActionRequestCallback implements HttpClient.RequestCallback { 221 | 222 | protected final ActionHolder holder; 223 | 224 | private ActionRequestCallback(ActionHolder holder) {this.holder = holder;} 225 | 226 | } 227 | 228 | } 229 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/RequestBuilder.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | import java.lang.reflect.Array; 5 | import java.net.MalformedURLException; 6 | import java.net.URL; 7 | import java.net.URLEncoder; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import io.techery.janet.body.ActionBody; 13 | import io.techery.janet.converter.Converter; 14 | import io.techery.janet.http.annotations.HttpAction; 15 | import io.techery.janet.http.model.FormUrlEncodedRequestBody; 16 | import io.techery.janet.http.model.Header; 17 | import io.techery.janet.http.model.MimeOverridingTypedOutput; 18 | import io.techery.janet.http.model.MultipartRequestBody; 19 | import io.techery.janet.http.model.Request; 20 | 21 | public final class RequestBuilder { 22 | 23 | private final Converter converter; 24 | private String url; 25 | 26 | private FormUrlEncodedRequestBody formBody; 27 | private MultipartRequestBody multipartBody; 28 | private ActionBody body; 29 | 30 | private StringBuilder queryParams; 31 | private List
headers; 32 | private String contentTypeHeader; 33 | private HttpAction.Method requestMethod = HttpAction.Method.GET; 34 | private String ref; 35 | 36 | RequestBuilder(String url, Converter converter) { 37 | this.url = url; 38 | this.converter = converter; 39 | this.multipartBody = null; 40 | this.formBody = null; 41 | } 42 | 43 | public void setRequestType(HttpAction.Type type) { 44 | switch (type) { 45 | case FORM_URL_ENCODED: 46 | formBody = new FormUrlEncodedRequestBody(); 47 | multipartBody = null; 48 | body = formBody; 49 | break; 50 | case MULTIPART: 51 | formBody = null; 52 | multipartBody = new MultipartRequestBody(); 53 | body = multipartBody; 54 | break; 55 | case SIMPLE: 56 | formBody = null; 57 | multipartBody = null; 58 | break; 59 | default: 60 | throw new IllegalArgumentException("Unknown request type: " + type); 61 | } 62 | } 63 | 64 | public void setUrl(Object param) { 65 | if (param == null) { 66 | throw new IllegalArgumentException("@Url field is null."); 67 | } 68 | String value = param.toString(); 69 | URL url = null; 70 | try { 71 | url = new URL(value); 72 | } catch (MalformedURLException ignored) {} 73 | if (url != null) { 74 | String s = url.toString(); 75 | this.url = s; 76 | if (url.getQuery() != null) { 77 | String query = "?" + url.getQuery(); 78 | this.url = s.substring(0, s.indexOf(query)); 79 | this.queryParams = new StringBuilder(query); 80 | } 81 | this.ref = url.getRef(); 82 | } else { 83 | setPath(value); 84 | } 85 | } 86 | 87 | public void addHeader(String name, String value) { 88 | if (name == null) { 89 | throw new IllegalArgumentException("Header name must not be null."); 90 | } 91 | if ("Content-Type".equalsIgnoreCase(name)) { 92 | contentTypeHeader = value; 93 | return; 94 | } 95 | 96 | List
headers = this.headers; 97 | if (headers == null) { 98 | this.headers = headers = new ArrayList
(2); 99 | } 100 | headers.add(new Header(name, value)); 101 | } 102 | 103 | public void setPath(String path) { 104 | StringBuilder url = new StringBuilder(this.url); 105 | if (this.url.endsWith("/")) { 106 | url.deleteCharAt(url.length() - 1); 107 | } 108 | if (path.startsWith("/")) { 109 | path = path.substring(1); 110 | } 111 | this.url = url.append("/").append(path).toString(); 112 | } 113 | 114 | public void addPathParam(String name, String value) { 115 | addPathParam(name, value, true); 116 | } 117 | 118 | public void addPathParam(String name, String value, boolean urlEncodeValue) { 119 | if (name == null) { 120 | throw new IllegalArgumentException("Path replacement name must not be null."); 121 | } 122 | if (value == null) { 123 | throw new IllegalArgumentException( 124 | "Path replacement \"" + name + "\" value must not be null."); 125 | } 126 | try { 127 | if (urlEncodeValue) { 128 | String encodedValue = URLEncoder.encode(String.valueOf(value), "UTF-8"); 129 | // URLEncoder encodes for use as a query parameter. Path encoding uses %20 to 130 | // encode spaces rather than +. Query encoding difference specified in HTML spec. 131 | // Any remaining plus signs represent spaces as already URLEncoded. 132 | encodedValue = encodedValue.replace("+", "%20"); 133 | url = url.replace("{" + name + "}", encodedValue); 134 | } else { 135 | url = url.replace("{" + name + "}", String.valueOf(value)); 136 | } 137 | } catch (UnsupportedEncodingException e) { 138 | throw new RuntimeException( 139 | "Unable to convert path parameter \"" + name + "\" value to UTF-8:" + value, e); 140 | } 141 | } 142 | 143 | public void addQueryParam(String name, Object value, boolean encodeName, boolean encodeValue) { 144 | if (value == null) return; 145 | // 146 | if (value instanceof Iterable) { 147 | name += "[]"; 148 | for (Object iterableValue : (Iterable) value) { 149 | if (iterableValue != null) { // Skip null values 150 | addQueryParam(name, iterableValue.toString(), encodeName, encodeValue); 151 | } 152 | } 153 | } else if (value.getClass().isArray()) { 154 | name += "[]"; 155 | for (int x = 0, arrayLength = Array.getLength(value); x < arrayLength; x++) { 156 | Object arrayValue = Array.get(value, x); 157 | if (arrayValue != null) { // Skip null values 158 | addQueryParam(name, arrayValue.toString(), encodeName, encodeValue); 159 | } 160 | } 161 | } else { 162 | addQueryParam(name, String.valueOf(value), encodeName, encodeValue); 163 | } 164 | } 165 | 166 | private void addQueryParam(String name, String value, boolean encodeName, boolean encodeValue) { 167 | if (name == null) { 168 | throw new IllegalArgumentException("Query param name must not be null."); 169 | } 170 | if (value == null) { 171 | throw new IllegalArgumentException("Query param \"" + name + "\" value must not be null."); 172 | } 173 | try { 174 | StringBuilder queryParams = this.queryParams; 175 | if (queryParams == null) { 176 | this.queryParams = queryParams = new StringBuilder(); 177 | } 178 | 179 | queryParams.append(queryParams.length() > 0 ? '&' : '?'); 180 | 181 | if (encodeName) { 182 | name = URLEncoder.encode(name, "UTF-8"); 183 | } 184 | if (encodeValue) { 185 | value = URLEncoder.encode(value, "UTF-8"); 186 | } 187 | queryParams.append(name).append('=').append(value); 188 | } catch (UnsupportedEncodingException e) { 189 | throw new RuntimeException( 190 | "Unable to convert query parameter \"" + name + "\" value to UTF-8: " + value, e); 191 | } 192 | } 193 | 194 | public void addField(String name, Object value) { 195 | if (value == null) return; 196 | // 197 | if (value instanceof Iterable) { 198 | name += "[]"; 199 | for (Object iterableValue : (Iterable) value) { 200 | if (iterableValue != null) { // Skip null values. 201 | formBody.addField(name, iterableValue.toString()); 202 | } 203 | } 204 | } else if (value.getClass().isArray()) { 205 | name += "[]"; 206 | for (int x = 0, arrayLength = Array.getLength(value); x < arrayLength; x++) { 207 | Object arrayValue = Array.get(value, x); 208 | if (arrayValue != null) { // Skip null values. 209 | formBody.addField(name, arrayValue.toString()); 210 | } 211 | } 212 | } else { 213 | formBody.addField(name, String.valueOf(value)); 214 | } 215 | } 216 | 217 | public void addPart(String name, String transferEncoding, MultipartRequestBody.PartBody body) { 218 | multipartBody.addPart(name, transferEncoding, body); 219 | } 220 | 221 | public void setBody(Object value) { 222 | if (multipartBody != null || formBody != null) { 223 | throw new IllegalArgumentException("Request is not simple type"); 224 | } 225 | if (value == null) { 226 | throw new IllegalArgumentException("Body parameter value must not be null."); 227 | } 228 | if (value instanceof ActionBody) { 229 | body = (ActionBody) value; 230 | } else { 231 | body = converter.toBody(value); 232 | } 233 | } 234 | 235 | public void setMethod(HttpAction.Method method) { 236 | this.requestMethod = method; 237 | } 238 | 239 | Request build() { 240 | if (multipartBody != null && multipartBody.getPartCount() == 0) { 241 | throw new IllegalStateException("Multipart requests must contain at least one part."); 242 | } 243 | StringBuilder url = new StringBuilder(this.url); 244 | StringBuilder queryParams = this.queryParams; 245 | if (queryParams != null) { 246 | url.append(queryParams); 247 | } 248 | if (ref != null) { 249 | url.append("#"); 250 | url.append(ref); 251 | } 252 | ActionBody body = this.body; 253 | List
headers = this.headers; 254 | if (contentTypeHeader != null) { 255 | if (body != null) { 256 | body = new MimeOverridingTypedOutput(body, contentTypeHeader); 257 | } else { 258 | Header header = new Header("Content-Type", contentTypeHeader); 259 | if (headers == null) { 260 | headers = Collections.singletonList(header); 261 | } else { 262 | headers.add(header); 263 | } 264 | } 265 | } 266 | return new Request(requestMethod.name(), url.toString(), headers, body); 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/HttpClient.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http; 2 | 3 | 4 | import java.io.IOException; 5 | 6 | import io.techery.janet.http.model.Request; 7 | import io.techery.janet.http.model.Response; 8 | 9 | /** 10 | * Abstraction of an HTTP client which can execute {@linkplain Request Requests}. This class must be 11 | * thread-safe as invocation may happen from multiple threads simultaneously. 12 | */ 13 | public interface HttpClient { 14 | 15 | int CONNECT_TIMEOUT_MILLIS = 15 * 1000; // 15s 16 | int READ_TIMEOUT_MILLIS = 20 * 1000; // 20s 17 | long PROGRESS_THRESHOLD = 1024; 18 | 19 | /** 20 | * Synchronously execute an HTTP represented by {@code request} and encapsulate all response data 21 | * into a {@linkplain Response} instance. 22 | */ 23 | Response execute(Request request, RequestCallback requestCallback) throws IOException; 24 | 25 | /** 26 | * Immediately cancel running request. 27 | *

28 | * Use {@linkplain Request#tag} for setting internal cancellation object on execution before to use it here. 29 | */ 30 | void cancel(Request request); 31 | 32 | interface RequestCallback { 33 | void onProgress(int progress); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/Body.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Documented 11 | @Target(FIELD) 12 | @Retention(RUNTIME) 13 | public @interface Body { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/Field.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Documented 11 | @Target(FIELD) 12 | @Retention(RUNTIME) 13 | public @interface Field { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/HttpAction.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.TYPE; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Documented 11 | @Target(TYPE) 12 | @Retention(RUNTIME) 13 | public @interface HttpAction { 14 | 15 | Method method() default Method.GET; 16 | 17 | String value() default ""; 18 | 19 | Type type() default Type.SIMPLE; 20 | 21 | enum Type { 22 | /** 23 | * No content-specific logic required. 24 | */ 25 | SIMPLE, 26 | /** 27 | * Multi-part request body. 28 | */ 29 | MULTIPART, 30 | /** 31 | * Form URL-encoded request body. 32 | */ 33 | FORM_URL_ENCODED 34 | } 35 | 36 | enum Method { 37 | GET(false), POST(true), PUT(true), DELETE(true), HEAD(false), PATCH(true); 38 | 39 | private boolean hasBody; 40 | 41 | Method(boolean hasBody) { 42 | this.hasBody = hasBody; 43 | } 44 | 45 | public boolean hasBody() { 46 | return hasBody; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/Part.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | 4 | import java.lang.annotation.Documented; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.Target; 7 | 8 | import io.techery.janet.http.model.MultipartRequestBody; 9 | 10 | import static java.lang.annotation.ElementType.FIELD; 11 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 12 | 13 | @Documented 14 | @Target(FIELD) 15 | @Retention(RUNTIME) 16 | public @interface Part { 17 | String value(); 18 | 19 | String encoding() default MultipartRequestBody.DEFAULT_TRANSFER_ENCODING; 20 | } 21 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/Path.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface Path { 14 | String value(); 15 | 16 | boolean encode() default true; 17 | } 18 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/Query.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Documented 11 | @Target(FIELD) 12 | @Retention(RUNTIME) 13 | public @interface Query { 14 | /** 15 | * The query parameter name. 16 | */ 17 | String value(); 18 | 19 | /** 20 | * Specifies whether {@link #value()} is URL encoded. 21 | */ 22 | boolean encodeName() default false; 23 | 24 | /** 25 | * Specifies whether the argument value to the annotated method parameter is URL encoded. 26 | */ 27 | boolean encodeValue() default true; 28 | } 29 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/RequestHeader.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface RequestHeader { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/Response.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | 11 | /** 12 | * Body for action response 13 | *

14 | * By default body is parsed only for successful response (200..300). 15 | * Other conditions are specified via arguments. 16 | *

17 | * For Example, 18 | *

    19 | *
  • {@code @Response(Response.ERROR)} - for unsuccessful response {@code (status >= 300)}
  • 20 | *
  • {@code @Response(401)} - for response with specific status. In example, the annotated field will be filled only for 21 | * response with status code 401
  • 22 | *
  • {@code @Response(min = 200, max = 202)} - for response with specific status range. In example, the annotated field 23 | * will be filled only for response with status code from 200 to 202 inclusive
  • 24 | *
  • {@code @Response(min = 300)} - if one of point of range is not set this point set as infinity. In example, 25 | * the field will be filled for response witch status code equals or greater than 300
  • 26 | *
27 | */ 28 | @Documented 29 | @Retention(RUNTIME) 30 | @Target(FIELD) 31 | public @interface Response { 32 | 33 | /** 34 | * HTTP status code of server response. 35 | *

36 | * To handle only successful response use {@code SUCCESS} or {@code ERROR} 37 | * to handle error response 38 | */ 39 | int value() default 0; 40 | 41 | int min() default 0; //from infinity 42 | 43 | int max() default 0; //to infinity 44 | 45 | /** for successful statuses (200..300)) */ 46 | int SUCCESS = -1; 47 | /** for unsuccessful statuses */ 48 | int ERROR = -2; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/ResponseHeader.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | @Documented 11 | @Retention(RUNTIME) 12 | @Target(FIELD) 13 | public @interface ResponseHeader { 14 | String value(); 15 | } 16 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/Status.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.ElementType.FIELD; 8 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 9 | 10 | /** 11 | * Response status code. 12 | * Possible types of field: 13 | * Boolean.class, Integer.class, Long.class, String.class, boolean.class, int.class, long.class 14 | */ 15 | @Documented 16 | @Retention(RUNTIME) 17 | @Target(FIELD) 18 | public @interface Status { 19 | } 20 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/annotations/Url.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.annotations; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import io.techery.janet.HttpActionService; 8 | 9 | import static java.lang.annotation.ElementType.FIELD; 10 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 11 | 12 | /** 13 | * URL resolved against the base URL which set to {@linkplain HttpActionService} 14 | *

15 | * Also it can be as a url path (@HttpAction.value()) if field value doesn't contain a scheme (http, https, ...). 16 | *

17 | * Annotated field can be only as String, java.net.URI, android.net.Uri, okhttp3.HttpUrl or com.squareup.okhttp.HttpUrl 18 | */ 19 | @Documented 20 | @Retention(RUNTIME) 21 | @Target(FIELD) 22 | public @interface Url {} 23 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/exception/HttpDeserializationException.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.exception; 2 | 3 | import io.techery.janet.converter.ConverterException; 4 | import io.techery.janet.http.model.Response; 5 | 6 | /** 7 | * Thrown to indicate that something went wrong with converting http response. 8 | * This class includes target {@link Response} 9 | */ 10 | public class HttpDeserializationException extends Exception { 11 | 12 | private final Response response; 13 | 14 | public HttpDeserializationException(ConverterException cause, Response response) { 15 | super(cause); 16 | this.response = response; 17 | } 18 | 19 | public Response getResponse() { 20 | return response; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/exception/HttpException.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.exception; 2 | 3 | 4 | import java.net.SocketTimeoutException; 5 | 6 | import io.techery.janet.http.model.Request; 7 | import io.techery.janet.http.model.Response; 8 | 9 | /** 10 | * Exception to indicate Http Request/Response fail. 11 | *

    12 | *
  • Fail on response (status code is out of 2xx) will contain both {@link Request} and {@link Response} models.
  • 13 | *
  • Fail on request (e.g. {@link SocketTimeoutException}) will contain {@link Request} and {@link Throwable} cause.
  • 14 | *
15 | * 16 | * See {@code isFailedOnRequest}, getRequest, getResponse, getCause for details. 17 | */ 18 | public class HttpException extends Exception { 19 | 20 | private final Request request; 21 | private final Response response; 22 | 23 | public static HttpException forRequest(Request request, Throwable cause) { 24 | return new HttpException(request, null, cause); 25 | } 26 | 27 | public static HttpException forResponse(Request request, Response response) { 28 | return new HttpException(request, response, null); 29 | } 30 | 31 | private HttpException(Request request, Response response, Throwable cause) { 32 | super(createMessage(request, response), cause); 33 | this.request = request; 34 | this.response = response; 35 | } 36 | 37 | private static String createMessage(Request request, Response response) { 38 | return response == null ? 39 | "HTTP call failed on request" : 40 | "HTTP call failed on response with status=" + response.getStatus() + ", with reason=" + response.getReason(); 41 | } 42 | 43 | public Request getRequest() { 44 | return request; 45 | } 46 | 47 | public Response getResponse() { 48 | return response; 49 | } 50 | 51 | public boolean isRequestFail() { 52 | return response == null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/exception/HttpSerializationException.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.exception; 2 | 3 | import io.techery.janet.converter.ConverterException; 4 | import io.techery.janet.http.model.Request; 5 | 6 | /** 7 | * Thrown to indicate that something went wrong with converting http request. 8 | * This class includes target {@link Request} 9 | */ 10 | public class HttpSerializationException extends Exception { 11 | 12 | private final Request request; 13 | 14 | public HttpSerializationException(ConverterException cause, Request request) { 15 | super(cause); 16 | this.request = request; 17 | } 18 | 19 | public Request getRequest() { 20 | return request; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/exception/HttpServiceException.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.exception; 2 | 3 | import io.techery.janet.JanetException; 4 | 5 | public class HttpServiceException extends JanetException { 6 | 7 | public HttpServiceException(Throwable cause) { 8 | super(cause); 9 | } 10 | 11 | public HttpServiceException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/internal/ProgressOutputStream.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.internal; 2 | 3 | import java.io.FilterOutputStream; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | 7 | import io.techery.janet.http.HttpClient; 8 | 9 | public final class ProgressOutputStream extends FilterOutputStream { 10 | 11 | private final ProgressListener listener; 12 | private final long threshold; 13 | private long progress; 14 | private long lastProgress; 15 | 16 | public ProgressOutputStream(OutputStream stream, ProgressListener listener, long threshold) { 17 | super(stream); 18 | this.listener = listener; 19 | this.threshold = threshold; 20 | } 21 | 22 | public ProgressOutputStream(OutputStream stream, ProgressListener listener) { 23 | this(stream, listener, HttpClient.PROGRESS_THRESHOLD); 24 | } 25 | 26 | @Override 27 | public void write(int b) throws IOException { 28 | super.write(b); 29 | progress++; 30 | if (progress > lastProgress + threshold) { 31 | listener.onProgressChanged(progress); 32 | lastProgress = progress; 33 | } 34 | } 35 | 36 | public long getThreshold() { 37 | return threshold; 38 | } 39 | 40 | public interface ProgressListener { 41 | void onProgressChanged(long bytesWritten); 42 | } 43 | } -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/model/FormUrlEncodedRequestBody.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.model; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.net.URLEncoder; 9 | 10 | import io.techery.janet.body.ActionBody; 11 | import io.techery.janet.body.util.StreamUtil; 12 | 13 | public final class FormUrlEncodedRequestBody extends ActionBody { 14 | 15 | private static final String MIMETYPE = "application/x-www-form-urlencoded; charset=UTF-8"; 16 | 17 | final ByteArrayOutputStream content = new ByteArrayOutputStream(); 18 | 19 | public FormUrlEncodedRequestBody() { 20 | super(MIMETYPE); 21 | } 22 | 23 | public void addField(String name, String value) { 24 | if (name == null) { 25 | throw new NullPointerException("name"); 26 | } 27 | if (value == null) { 28 | throw new NullPointerException("value"); 29 | } 30 | if (content.size() > 0) { 31 | content.write('&'); 32 | } 33 | try { 34 | name = URLEncoder.encode(name, "UTF-8"); 35 | value = URLEncoder.encode(value, "UTF-8"); 36 | 37 | content.write(name.getBytes("UTF-8")); 38 | content.write('='); 39 | content.write(value.getBytes("UTF-8")); 40 | } catch (IOException e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | 45 | @Override public long length() { 46 | return content.size(); 47 | } 48 | 49 | @Override public InputStream getContent() throws IOException { 50 | return new ByteArrayInputStream(content.toByteArray()); 51 | } 52 | 53 | @Override public void writeContentTo(OutputStream os) throws IOException { 54 | StreamUtil.writeAll(getContent(), os, StreamUtil.NETWORK_CHUNK_SIZE); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/model/Header.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.model; 2 | 3 | public final class Header { 4 | private final String name; 5 | private final String value; 6 | 7 | public Header(String name, String value) { 8 | this.name = name; 9 | this.value = value; 10 | } 11 | 12 | public String getName() { 13 | return name; 14 | } 15 | 16 | public String getValue() { 17 | return value; 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) return true; 23 | if (o == null || getClass() != o.getClass()) return false; 24 | 25 | Header header = (Header) o; 26 | 27 | if (name != null ? !name.equals(header.name) : header.name != null) return false; 28 | if (value != null ? !value.equals(header.value) : header.value != null) return false; 29 | 30 | return true; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | int result = name != null ? name.hashCode() : 0; 36 | result = 31 * result + (value != null ? value.hashCode() : 0); 37 | return result; 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return (name != null ? name : "") + ": " + (value != null ? value : ""); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/model/MimeOverridingTypedOutput.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.model; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | 7 | import io.techery.janet.body.ActionBody; 8 | 9 | public class MimeOverridingTypedOutput extends ActionBody { 10 | 11 | private final ActionBody delegate; 12 | 13 | public MimeOverridingTypedOutput(ActionBody delegate, String mimeType) { 14 | super(mimeType); 15 | if (delegate == null) throw new NullPointerException("Delegate is null"); 16 | this.delegate = delegate; 17 | } 18 | 19 | @Override public long length() { 20 | return delegate.length(); 21 | } 22 | 23 | @Override public InputStream getContent() throws IOException { 24 | return delegate.getContent(); 25 | } 26 | 27 | @Override public void writeContentTo(OutputStream os) throws IOException { 28 | delegate.writeContentTo(os); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/model/MultipartRequestBody.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.model; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.io.SequenceInputStream; 9 | import java.util.ArrayList; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | import java.util.UUID; 15 | 16 | import io.techery.janet.body.ActionBody; 17 | 18 | public final class MultipartRequestBody extends ActionBody { 19 | 20 | public static final String MIMETYPE_FORM_DATA = "multipart/form-data"; 21 | public static final String DEFAULT_TRANSFER_ENCODING = "binary"; 22 | 23 | private static final String DASH_DASH = "--"; 24 | private static final String CRLF = "\r\n"; 25 | 26 | private final List mimeParts = new LinkedList(); 27 | 28 | private final String boundary; 29 | private final byte[] footer; 30 | private long length; 31 | 32 | public MultipartRequestBody() { 33 | this(UUID.randomUUID().toString()); 34 | } 35 | 36 | MultipartRequestBody(String boundary) { 37 | super(MIMETYPE_FORM_DATA + "; boundary=" + boundary); //TODO add support for other mime types 38 | this.boundary = boundary; 39 | footer = buildBoundary(boundary, false, true); 40 | length = footer.length; 41 | } 42 | 43 | @Override 44 | public long length() { 45 | return length; 46 | } 47 | 48 | @Override 49 | public InputStream getContent() throws IOException { 50 | List streams = new ArrayList(mimeParts.size()); 51 | for (MimePart mimePart : mimeParts) streams.add(mimePart.getContent()); 52 | streams.add(new ByteArrayInputStream(footer)); 53 | return new SequenceInputStream(Collections.enumeration(streams)); 54 | } 55 | 56 | @Override public void writeContentTo(OutputStream os) throws IOException { 57 | for (MimePart part : mimeParts) part.writeTo(os); 58 | os.write(footer); 59 | } 60 | 61 | public void addPart(String name, String transferEncoding, PartBody body) { 62 | if (name == null) { 63 | throw new NullPointerException("Part name must not be null."); 64 | } 65 | if (transferEncoding == null) { 66 | transferEncoding = DEFAULT_TRANSFER_ENCODING; 67 | } 68 | if (body == null) { 69 | throw new NullPointerException("Part body must not be null."); 70 | } 71 | 72 | MimePart part = new MimePart(name, transferEncoding, body, boundary, mimeParts.isEmpty()); 73 | mimeParts.add(part); 74 | 75 | long size = part.size(); 76 | if (size == -1) { 77 | length = -1; 78 | } else if (length != -1) { 79 | length += size; 80 | } 81 | } 82 | 83 | List getParts() throws IOException { 84 | List parts = new ArrayList(mimeParts.size()); 85 | for (MimePart part : mimeParts) { 86 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 87 | part.writeTo(bos); 88 | parts.add(bos.toByteArray()); 89 | } 90 | return parts; 91 | } 92 | 93 | public int getPartCount() { 94 | return mimeParts.size(); 95 | } 96 | 97 | private static final class MimePart { 98 | private final String name; 99 | private final String transferEncoding; 100 | private final PartBody bodyWrapper; 101 | private final boolean isFirst; 102 | private final String boundary; 103 | 104 | private byte[] partBoundary; 105 | private byte[] partHeader; 106 | private boolean isBuilt; 107 | 108 | public MimePart(String name, String transferEncoding, PartBody bodyWrapper, String boundary, boolean isFirst) { 109 | this.name = name; 110 | this.transferEncoding = transferEncoding; 111 | this.bodyWrapper = bodyWrapper; 112 | this.isFirst = isFirst; 113 | this.boundary = boundary; 114 | } 115 | 116 | public InputStream getContent() throws IOException { 117 | build(); 118 | List streams = Arrays.asList( 119 | new ByteArrayInputStream(partBoundary), 120 | new ByteArrayInputStream(partHeader), 121 | bodyWrapper.body.getContent() 122 | ); 123 | return new SequenceInputStream(Collections.enumeration(streams)); 124 | } 125 | 126 | public void writeTo(OutputStream out) throws IOException { 127 | build(); 128 | out.write(partBoundary); 129 | out.write(partHeader); 130 | bodyWrapper.body.writeContentTo(out); 131 | } 132 | 133 | public long size() { 134 | build(); 135 | if (bodyWrapper.body.length() > -1) { 136 | return bodyWrapper.body.length() + partBoundary.length + partHeader.length; 137 | } else { 138 | return -1; 139 | } 140 | } 141 | 142 | private void build() { 143 | if (isBuilt) return; 144 | partBoundary = buildBoundary(boundary, isFirst, false); 145 | partHeader = buildHeader(name, transferEncoding, bodyWrapper.headers, bodyWrapper.body); 146 | isBuilt = true; 147 | } 148 | 149 | private static byte[] buildHeader(String name, String transferEncoding, List
headers, ActionBody value) { 150 | try { 151 | StringBuilder result = new StringBuilder(); 152 | 153 | result.append("Content-Disposition: form-data"); 154 | result.append("; name="); 155 | appendQuotedString(result, name); 156 | for (Header header : headers) { 157 | if (header.getName().equals("filename")) { 158 | result.append("; filename="); 159 | appendQuotedString(result, header.getValue()); 160 | break; 161 | } 162 | } 163 | result.append(CRLF); 164 | 165 | result.append("Content-Type: ").append(value.mimeType()).append(CRLF); 166 | 167 | long length = value.length(); 168 | if (length != -1) { 169 | result.append("Content-Length: ").append(length).append(CRLF); 170 | } 171 | 172 | result.append("Content-Transfer-Encoding: ").append(transferEncoding).append(CRLF); 173 | 174 | // additional headers 175 | for (Header header : headers) { 176 | if (header.getName().equals("filename")) continue; 177 | result.append(header.toString()).append(CRLF); 178 | } 179 | 180 | result.append(CRLF); 181 | 182 | return result.toString().getBytes("UTF-8"); 183 | } catch (IOException ex) { 184 | throw new RuntimeException("Unable to write multipart header", ex); 185 | } 186 | } 187 | 188 | static StringBuilder appendQuotedString(StringBuilder target, String key) { 189 | target.append('"'); 190 | for (int i = 0, len = key.length(); i < len; i++) { 191 | char ch = key.charAt(i); 192 | switch (ch) { 193 | case '\n': 194 | target.append("%0A"); 195 | break; 196 | case '\r': 197 | target.append("%0D"); 198 | break; 199 | case '"': 200 | target.append("%22"); 201 | break; 202 | default: 203 | target.append(ch); 204 | break; 205 | } 206 | } 207 | target.append('"'); 208 | return target; 209 | } 210 | 211 | } 212 | 213 | public static class PartBody { 214 | public final ActionBody body; 215 | public final List
headers; 216 | 217 | protected PartBody(ActionBody body, List
headers) { 218 | if (body == null) throw new IllegalArgumentException("body can't be null"); 219 | this.body = body; 220 | if (headers == null) headers = Collections.emptyList(); 221 | this.headers = headers; 222 | } 223 | 224 | public static class Builder { 225 | private ActionBody body; 226 | private List
headers; 227 | 228 | public Builder setBody(ActionBody body) { 229 | this.body = body; 230 | return this; 231 | } 232 | 233 | public Builder addHeader(String name, String value) { 234 | if (headers == null) headers = new ArrayList
(); 235 | headers.add(new Header(name, value)); 236 | return this; 237 | } 238 | 239 | public PartBody build() { 240 | return new PartBody(body, headers); 241 | } 242 | } 243 | } 244 | 245 | private static byte[] buildBoundary(String boundary, boolean first, boolean last) { 246 | try { 247 | StringBuilder sb = new StringBuilder(boundary.length() + 8); 248 | 249 | if (!first) { 250 | sb.append(CRLF); 251 | } 252 | sb.append(DASH_DASH); 253 | sb.append(boundary); 254 | if (last) { 255 | sb.append(DASH_DASH); 256 | } 257 | sb.append(CRLF); 258 | return sb.toString().getBytes("UTF-8"); 259 | } catch (IOException ex) { 260 | throw new RuntimeException("Unable to write multipart boundary", ex); 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/model/Request.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import io.techery.janet.body.ActionBody; 8 | 9 | 10 | public final class Request { 11 | 12 | private final String method; 13 | private final String url; 14 | private final List
headers; 15 | private final ActionBody body; 16 | /** 17 | * Some object to mark a request. 18 | * For example, it'is using for cancellation in OkClient, ApacheClient, UrlConnectionClient 19 | */ 20 | public volatile Object tag; 21 | 22 | public Request(String method, String url, List
headers, ActionBody body) { 23 | if (method == null) { 24 | throw new NullPointerException("Method must not be null."); 25 | } 26 | if (url == null) { 27 | throw new NullPointerException("URL must not be null."); 28 | } 29 | this.method = method; 30 | this.url = url; 31 | 32 | if (headers == null) { 33 | this.headers = Collections.emptyList(); 34 | } else { 35 | this.headers = Collections.unmodifiableList(new ArrayList
(headers)); 36 | } 37 | 38 | this.body = body; 39 | } 40 | 41 | public String getMethod() { 42 | return method; 43 | } 44 | 45 | public String getUrl() { 46 | return url; 47 | } 48 | 49 | public List
getHeaders() { 50 | return headers; 51 | } 52 | 53 | public ActionBody getBody() { 54 | return body; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/model/Response.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.model; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import io.techery.janet.body.ActionBody; 8 | 9 | public final class Response { 10 | private final String url; 11 | private final int status; 12 | private final String reason; 13 | private final List
headers; 14 | private final ActionBody body; 15 | 16 | public Response(String url, int status, String reason, List
headers, ActionBody body) { 17 | if (url == null) { 18 | throw new IllegalArgumentException("url == null"); 19 | } 20 | if (status < 200) { 21 | throw new IllegalArgumentException("Invalid status code: " + status); 22 | } 23 | if (reason == null) { 24 | throw new IllegalArgumentException("reason == null"); 25 | } 26 | if (headers == null) { 27 | throw new IllegalArgumentException("headers == null"); 28 | } 29 | 30 | this.url = url; 31 | this.status = status; 32 | this.reason = reason; 33 | this.headers = Collections.unmodifiableList(new ArrayList
(headers)); 34 | this.body = body; 35 | } 36 | 37 | public String getUrl() { 38 | return url; 39 | } 40 | 41 | public int getStatus() { 42 | return status; 43 | } 44 | 45 | public boolean isSuccessful() { 46 | return status >= 200 && status < 300; 47 | } 48 | 49 | public String getReason() { 50 | return reason; 51 | } 52 | 53 | public List
getHeaders() { 54 | return headers; 55 | } 56 | 57 | public ActionBody getBody() { 58 | return body; 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "Response{" + 64 | "url='" + url + '\'' + 65 | ", status=" + status + 66 | ", reason='" + reason + '\'' + 67 | ", headers=" + headers + 68 | ", body=" + body + 69 | '}'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/test/MockHttpActionService.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.test; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.OutputStream; 7 | import java.lang.reflect.Type; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import io.techery.janet.ActionHolder; 13 | import io.techery.janet.ActionService; 14 | import io.techery.janet.ActionServiceWrapper; 15 | import io.techery.janet.HttpActionService; 16 | import io.techery.janet.JanetException; 17 | import io.techery.janet.body.ActionBody; 18 | import io.techery.janet.converter.Converter; 19 | import io.techery.janet.converter.ConverterException; 20 | import io.techery.janet.http.HttpClient; 21 | import io.techery.janet.http.model.Header; 22 | import io.techery.janet.http.model.Request; 23 | import rx.functions.Func1; 24 | import rx.functions.Func2; 25 | 26 | public final class MockHttpActionService extends ActionServiceWrapper { 27 | 28 | private MockHttpActionService(List contracts) { 29 | this(new HttpActionService("https://github.com/techery/janet", new MockClient(contracts), new MockConverter())); 30 | } 31 | 32 | private MockHttpActionService(ActionService actionService) { 33 | super(actionService); 34 | } 35 | 36 | public final static class Builder { 37 | 38 | private Func2 actionServiceFunc = null; 39 | private final List contracts = new ArrayList(); 40 | 41 | public Builder bind(Response response, Func1 predicate) { 42 | if (response == null) { 43 | throw new IllegalArgumentException("response == null"); 44 | } 45 | if (predicate == null) { 46 | throw new IllegalArgumentException("predicate == null"); 47 | } 48 | contracts.add(new Contract(predicate, response)); 49 | return this; 50 | } 51 | 52 | /** 53 | * In case if you want to build your testing around some real service (e.g. if it does some additional logic, 54 | * sufficient for your needs) - instantiate and return it withing given Func2, passing forward two parameters: 55 | * HttpClient and Converter 56 | * 57 | * @param actionServiceFunc func that will instantiate service to wrap in a predefined way 58 | * @return current instance of Builder 59 | */ 60 | public Builder wrapService(Func2 actionServiceFunc) { 61 | this.actionServiceFunc = actionServiceFunc; 62 | return this; 63 | } 64 | 65 | public MockHttpActionService build() { 66 | if (actionServiceFunc == null) { 67 | return new MockHttpActionService(contracts); 68 | } else { 69 | return new MockHttpActionService(actionServiceFunc.call(new MockClient(contracts), new MockConverter())); 70 | } 71 | } 72 | } 73 | 74 | private final static class MockClient implements HttpClient { 75 | 76 | private final List contracts; 77 | 78 | private MockClient(List contracts) { 79 | this.contracts = contracts; 80 | } 81 | 82 | @Override 83 | public io.techery.janet.http.model.Response execute(Request request, RequestCallback requestCallback) throws IOException { 84 | for (Contract contract : contracts) { 85 | if (contract.predicate.call(request)) { 86 | Response response = contract.response; 87 | return new io.techery.janet.http.model.Response(request.getUrl(), response.status, response.reason, response.headers, new MockActionBody(response.body)); 88 | } 89 | } 90 | throw new UnsupportedOperationException("There is no contract for " + request); 91 | } 92 | 93 | @Override public void cancel(Request request) {} 94 | } 95 | 96 | private final static class MockConverter implements Converter { 97 | 98 | @Override public Object fromBody(ActionBody body, Type type) throws ConverterException { 99 | if (body instanceof MockActionBody) { 100 | return ((MockActionBody) body).body; 101 | } 102 | throw new UnsupportedOperationException("Something went happened with mock response. Couldn't convert " + body); 103 | } 104 | 105 | @Override public ActionBody toBody(Object object) throws ConverterException { 106 | return new EmptyActionBody(); 107 | } 108 | } 109 | 110 | private final static class MockActionBody extends EmptyActionBody { 111 | private final Object body; 112 | 113 | private MockActionBody(Object body) { 114 | this.body = body; 115 | } 116 | } 117 | 118 | private static class EmptyActionBody extends ActionBody { 119 | 120 | public EmptyActionBody() { 121 | super(null); 122 | } 123 | 124 | @Override public long length() { 125 | return 0L; 126 | } 127 | 128 | @Override public InputStream getContent() throws IOException { 129 | return new ByteArrayInputStream(new byte[0]); 130 | } 131 | 132 | @Override public void writeContentTo(OutputStream os) throws IOException { 133 | } 134 | } 135 | 136 | private final static class Contract { 137 | private final Func1 predicate; 138 | private final Response response; 139 | 140 | private Contract(Func1 predicate, Response response) { 141 | this.predicate = predicate; 142 | this.response = response; 143 | } 144 | } 145 | 146 | public static class Response { 147 | private final int status; 148 | private final String reason; 149 | private final List
headers; 150 | private Object body; 151 | 152 | public Response(int status) { 153 | this.status = status; 154 | this.reason = ""; 155 | this.headers = new ArrayList
(); 156 | } 157 | 158 | public Response body(Object body) { 159 | if (body == null) { 160 | throw new IllegalArgumentException("body == null"); 161 | } 162 | this.body = body; 163 | return this; 164 | } 165 | 166 | public Response addHeader(Header... headers) { 167 | if (headers == null) { 168 | throw new IllegalArgumentException("headers == null"); 169 | } 170 | Collections.addAll(this.headers, headers); 171 | return this; 172 | } 173 | 174 | public Response reason(String reason) { 175 | if (reason == null) { 176 | throw new IllegalArgumentException("reason == null"); 177 | } 178 | return this; 179 | } 180 | } 181 | 182 | @Override protected boolean onInterceptSend(ActionHolder holder) throws JanetException { 183 | return false; 184 | } 185 | 186 | @Override protected void onInterceptCancel(ActionHolder holder) {} 187 | 188 | @Override protected void onInterceptStart(ActionHolder holder) {} 189 | 190 | @Override protected void onInterceptProgress(ActionHolder holder, int progress) {} 191 | 192 | @Override protected void onInterceptSuccess(ActionHolder holder) {} 193 | 194 | @Override protected boolean onInterceptFail(ActionHolder holder, JanetException e) { 195 | return false; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /service/src/main/java/io/techery/janet/http/utils/RequestUtils.java: -------------------------------------------------------------------------------- 1 | package io.techery.janet.http.utils; 2 | 3 | import java.io.IOException; 4 | 5 | import io.techery.janet.http.model.Request; 6 | 7 | public class RequestUtils { 8 | 9 | public static final Object TAG_CANCELED = new Object(); 10 | 11 | public static void throwIfCanceled(Request request) throws IOException { 12 | if (request.tag == TAG_CANCELED) { 13 | throw new IOException("Request is canceled"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':service' 2 | include ':service-compiler' 3 | include ':clients:client-android-apache-client' 4 | include ':clients:client-okhttp' 5 | include ':clients:client-okhttp3' 6 | include ':clients:client-url-connection' 7 | include ':sample' 8 | --------------------------------------------------------------------------------