/* * Copyright 2016 LINE Corporation * * LINE Corporation licenses this file to you under the Apache License, * version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.linecorp.armeria.client.http.retrofit2; import java.io.IOException; import java.net.URL; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import com.linecorp.armeria.client.http.HttpClient; import com.linecorp.armeria.common.http.HttpHeaderNames; import com.linecorp.armeria.common.http.HttpHeaders; import com.linecorp.armeria.common.http.HttpMethod; import com.linecorp.armeria.common.http.HttpResponse; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okio.Buffer; /** * A {@link Call.Factory} that creates a {@link Call} instance for {@link HttpClient}. */ public class ArmeriaCallFactory implements Call.Factory { private final HttpClient httpClient; /** * Creates a {@link Call.Factory} using the specified {@link HttpClient} instance. * * @param httpClient The {@link HttpClient} instance to be used. */ public ArmeriaCallFactory(HttpClient httpClient) { this.httpClient = httpClient; } @Override public Call newCall(Request request) { return new ArmeriaCall(this, request); } static class ArmeriaCall implements Call { private enum ExecutionState { IDLE, RUNNING, CANCELED, FINISHED } private static final AtomicReferenceFieldUpdater<ArmeriaCall, ExecutionState> executionStateUpdater = AtomicReferenceFieldUpdater.newUpdater(ArmeriaCall.class, ExecutionState.class, "executionState"); private final ArmeriaCallFactory callFactory; private final Request request; private volatile HttpResponse httpResponse; private volatile ExecutionState executionState = ExecutionState.IDLE; ArmeriaCall(ArmeriaCallFactory callFactory, Request request) { this.callFactory = callFactory; this.request = request; } private static HttpResponse doCall(HttpClient httpClient, Request request) { URL url = request.url().url(); StringBuilder uriBuilder = new StringBuilder(url.getPath()); if (url.getQuery() != null) { uriBuilder.append('?').append(url.getQuery()); } String uri = uriBuilder.toString(); final HttpHeaders headers; switch (request.method()) { case "GET": headers = HttpHeaders.of(HttpMethod.GET, uri); break; case "HEAD": headers = HttpHeaders.of(HttpMethod.HEAD, uri); break; case "POST": headers = HttpHeaders.of(HttpMethod.POST, uri); break; case "DELETE": headers = HttpHeaders.of(HttpMethod.DELETE, uri); break; case "PUT": headers = HttpHeaders.of(HttpMethod.PUT, uri); break; case "PATCH": headers = HttpHeaders.of(HttpMethod.PATCH, uri); break; case "OPTIONS": headers = HttpHeaders.of(HttpMethod.OPTIONS, uri); break; default: throw new IllegalArgumentException("Invalid HTTP method:" + request.method()); } request.headers().toMultimap().forEach( (key, values) -> headers.add(HttpHeaderNames.of(key), values)); final RequestBody body = request.body(); if (body != null) { final MediaType contentType = body.contentType(); if (contentType != null) { headers.set(HttpHeaderNames.CONTENT_TYPE, contentType.toString()); } try (Buffer contentBuffer = new Buffer()) { body.writeTo(contentBuffer); return httpClient.execute(headers, contentBuffer.readByteArray()); } catch (IOException e) { throw new IllegalArgumentException( "Failed to convert RequestBody to HttpData. " + request.method(), e); } } return httpClient.execute(headers); } @Override public Request request() { return request; } private synchronized void createRequest() { if (httpResponse != null) { throw new IllegalStateException("executed already"); } executionStateUpdater.compareAndSet(this, ExecutionState.IDLE, ExecutionState.RUNNING); httpResponse = doCall(callFactory.httpClient, request); } @Override public Response execute() throws IOException { CompletableCallback completableCallback = new CompletableCallback(); enqueue(completableCallback); try { return completableCallback.join(); } catch (CancellationException e) { throw new IOException(e); } catch (CompletionException e) { throw new IOException(e.getCause()); } } @Override public void enqueue(Callback callback) { createRequest(); httpResponse.subscribe(new ArmeriaCallSubscriber(this, callback, request)); } @Override public void cancel() { executionStateUpdater.set(this, ExecutionState.CANCELED); } @Override public boolean isExecuted() { return httpResponse != null; } @Override public boolean isCanceled() { return executionState == ExecutionState.CANCELED; } boolean tryFinish() { return executionStateUpdater.compareAndSet(this, ExecutionState.IDLE, ExecutionState.FINISHED) || executionStateUpdater.compareAndSet(this, ExecutionState.RUNNING, ExecutionState.FINISHED); } @Override public Call clone() { return new ArmeriaCall(callFactory, request); } } }