Skip to content

Commit

Permalink
Java http send method (#987)
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-james-dev authored Jul 21, 2023
1 parent b206771 commit db276f8
Show file tree
Hide file tree
Showing 11 changed files with 2,199 additions and 22 deletions.
4 changes: 4 additions & 0 deletions pkgs/java_http/jnigen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ class_path:
- 'classes.jar'

classes:
- 'java.io.InputStream'
- 'java.lang.System'
- 'java.net.HttpURLConnection'
- 'java.net.URL'
- 'java.net.URLConnection'
136 changes: 126 additions & 10 deletions pkgs/java_http/lib/src/java_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';

import 'package:http/http.dart';
import 'package:jni/jni.dart';
import 'package:path/path.dart';

import 'third_party/java/lang/System.dart';
import 'third_party/java/net/HttpURLConnection.dart';
import 'third_party/java/net/URL.dart';

// TODO: Add a description of the implementation.
Expand All @@ -20,6 +23,12 @@ class JavaClient extends BaseClient {
if (!Platform.isAndroid) {
Jni.spawnIfNotExists(dylibDir: join('build', 'jni_libs'));
}

// TODO: Determine if we can remove this.
// It's a workaround to fix the tests not passing on GitHub CI.
// See https://github.com/dart-lang/http/pull/987#issuecomment-1636170371.
System.setProperty(
'java.net.preferIPv6Addresses'.toJString(), 'true'.toJString());
}

@override
Expand All @@ -28,16 +37,123 @@ class JavaClient extends BaseClient {
// See https://github.com/dart-lang/http/pull/980#discussion_r1253700470.
_initJVM();

final javaUrl = URL.ctor3(request.url.toString().toJString());
final dartUrl = Uri.parse(javaUrl.toString1().toDartString());
final (statusCode, reasonPhrase, responseHeaders, responseBody) =
await Isolate.run(() {
request.finalize();

final httpUrlConnection = URL
.ctor3(request.url.toString().toJString())
.openConnection()
.castTo(HttpURLConnection.type, deleteOriginal: true);

request.headers.forEach((headerName, headerValue) {
httpUrlConnection.setRequestProperty(
headerName.toJString(), headerValue.toJString());
});

httpUrlConnection.setRequestMethod(request.method.toJString());

final statusCode = _statusCode(request, httpUrlConnection);
final reasonPhrase = _reasonPhrase(httpUrlConnection);
final responseHeaders = _responseHeaders(httpUrlConnection);
final responseBody = _responseBody(httpUrlConnection);

httpUrlConnection.disconnect();

return (
statusCode,
reasonPhrase,
responseHeaders,
responseBody,
);
});

return StreamedResponse(Stream.value(responseBody), statusCode,
contentLength: _contentLengthHeader(request, responseHeaders),
request: request,
headers: responseHeaders,
reasonPhrase: reasonPhrase);
}

int _statusCode(BaseRequest request, HttpURLConnection httpUrlConnection) {
final statusCode = httpUrlConnection.getResponseCode();

if (statusCode == -1) {
throw ClientException(
'Status code can not be discerned from the response.', request.url);
}

return statusCode;
}

String? _reasonPhrase(HttpURLConnection httpUrlConnection) {
final reasonPhrase = httpUrlConnection.getResponseMessage();

return reasonPhrase.isNull
? null
: reasonPhrase.toDartString(deleteOriginal: true);
}

Map<String, String> _responseHeaders(HttpURLConnection httpUrlConnection) {
final headers = <String, List<String>>{};

for (var i = 0;; i++) {
final headerName = httpUrlConnection.getHeaderFieldKey(i);
final headerValue = httpUrlConnection.getHeaderField1(i);

// If the header name and header value are both null then we have reached
// the end of the response headers.
if (headerName.isNull && headerValue.isNull) break;

// The HTTP response header status line is returned as a header field
// where the field key is null and the field is the status line.
// Other package:http implementations don't include the status line as a
// header. So we don't add the status line to the headers.
if (headerName.isNull) continue;

headers
.putIfAbsent(headerName.toDartString(), () => [])
.add(headerValue.toDartString());
}

return headers
.map((key, value) => MapEntry(key.toLowerCase(), value.join(',')));
}

int? _contentLengthHeader(BaseRequest request, Map<String, String> headers) {
final contentLengthHeader = headers['content-length'];

// Return null if the content length header is not set.
if (contentLengthHeader == null) return null;

// Throw ClientException if the content length header is not an integer.
final contentLength = int.tryParse(contentLengthHeader);
if (contentLength == null) {
throw ClientException(
'Invalid content-length header: $contentLengthHeader. '
'Content-length must be a non-negative integer.',
request.url,
);
}

return contentLength;
}

Uint8List _responseBody(HttpURLConnection httpUrlConnection) {
final responseCode = httpUrlConnection.getResponseCode();

final inputStream = (responseCode >= 200 && responseCode <= 299)
? httpUrlConnection.getInputStream()
: httpUrlConnection.getErrorStream();

final bytes = <int>[];
int byte;
while ((byte = inputStream.read()) != -1) {
bytes.add(byte);
}

const result = 'Hello World!';
final stream = Stream.value(latin1.encode(result));
inputStream.close();

return StreamedResponse(stream, 200,
contentLength: 12,
request: Request(request.method, dartUrl),
headers: {'content-type': 'text/plain'},
reasonPhrase: 'OK');
return Uint8List.fromList(bytes);
}
}
224 changes: 224 additions & 0 deletions pkgs/java_http/lib/src/third_party/java/io/InputStream.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// Autogenerated by jnigen. DO NOT EDIT!

// ignore_for_file: annotate_overrides
// ignore_for_file: camel_case_extensions
// ignore_for_file: camel_case_types
// ignore_for_file: constant_identifier_names
// ignore_for_file: file_names
// ignore_for_file: no_leading_underscores_for_local_identifiers
// ignore_for_file: non_constant_identifier_names
// ignore_for_file: overridden_fields
// ignore_for_file: unnecessary_cast
// ignore_for_file: unused_element
// ignore_for_file: unused_field
// ignore_for_file: unused_import
// ignore_for_file: unused_shown_name

import "dart:isolate" show ReceivePort;
import "dart:ffi" as ffi;
import "package:jni/internal_helpers_for_jnigen.dart";
import "package:jni/jni.dart" as jni;

/// from: java.io.InputStream
class InputStream extends jni.JObject {
@override
late final jni.JObjType<InputStream> $type = type;

InputStream.fromRef(
jni.JObjectPtr ref,
) : super.fromRef(ref);

static final _class = jni.Jni.findJClass(r"java/io/InputStream");

/// The type which includes information such as the signature of this class.
static const type = $InputStreamType();
static final _id_ctor =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"<init>", r"()V");

/// from: public void <init>()
/// The returned object must be deleted after use, by calling the `delete` method.
factory InputStream() {
return InputStream.fromRef(jni.Jni.accessors
.newObjectWithArgs(_class.reference, _id_ctor, []).object);
}

static final _id_nullInputStream = jni.Jni.accessors.getStaticMethodIDOf(
_class.reference, r"nullInputStream", r"()Ljava/io/InputStream;");

/// from: static public java.io.InputStream nullInputStream()
/// The returned object must be deleted after use, by calling the `delete` method.
static InputStream nullInputStream() {
return const $InputStreamType().fromRef(jni.Jni.accessors
.callStaticMethodWithArgs(_class.reference, _id_nullInputStream,
jni.JniCallType.objectType, []).object);
}

static final _id_read =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"read", r"()I");

/// from: public abstract int read()
int read() {
return jni.Jni.accessors.callMethodWithArgs(
reference, _id_read, jni.JniCallType.intType, []).integer;
}

static final _id_read1 =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"read", r"([B)I");

/// from: public int read(byte[] bs)
int read1(
jni.JArray<jni.jbyte> bs,
) {
return jni.Jni.accessors.callMethodWithArgs(
reference, _id_read1, jni.JniCallType.intType, [bs.reference]).integer;
}

static final _id_read2 =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"read", r"([BII)I");

/// from: public int read(byte[] bs, int i, int i1)
int read2(
jni.JArray<jni.jbyte> bs,
int i,
int i1,
) {
return jni.Jni.accessors.callMethodWithArgs(
reference,
_id_read2,
jni.JniCallType.intType,
[bs.reference, jni.JValueInt(i), jni.JValueInt(i1)]).integer;
}

static final _id_readAllBytes = jni.Jni.accessors
.getMethodIDOf(_class.reference, r"readAllBytes", r"()[B");

/// from: public byte[] readAllBytes()
/// The returned object must be deleted after use, by calling the `delete` method.
jni.JArray<jni.jbyte> readAllBytes() {
return const jni.JArrayType(jni.jbyteType()).fromRef(jni.Jni.accessors
.callMethodWithArgs(reference, _id_readAllBytes,
jni.JniCallType.objectType, []).object);
}

static final _id_readNBytes = jni.Jni.accessors
.getMethodIDOf(_class.reference, r"readNBytes", r"(I)[B");

/// from: public byte[] readNBytes(int i)
/// The returned object must be deleted after use, by calling the `delete` method.
jni.JArray<jni.jbyte> readNBytes(
int i,
) {
return const jni.JArrayType(jni.jbyteType()).fromRef(jni.Jni.accessors
.callMethodWithArgs(reference, _id_readNBytes,
jni.JniCallType.objectType, [jni.JValueInt(i)]).object);
}

static final _id_readNBytes1 = jni.Jni.accessors
.getMethodIDOf(_class.reference, r"readNBytes", r"([BII)I");

/// from: public int readNBytes(byte[] bs, int i, int i1)
int readNBytes1(
jni.JArray<jni.jbyte> bs,
int i,
int i1,
) {
return jni.Jni.accessors.callMethodWithArgs(
reference,
_id_readNBytes1,
jni.JniCallType.intType,
[bs.reference, jni.JValueInt(i), jni.JValueInt(i1)]).integer;
}

static final _id_skip =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"skip", r"(J)J");

/// from: public long skip(long j)
int skip(
int j,
) {
return jni.Jni.accessors.callMethodWithArgs(
reference, _id_skip, jni.JniCallType.longType, [j]).long;
}

static final _id_available =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"available", r"()I");

/// from: public int available()
int available() {
return jni.Jni.accessors.callMethodWithArgs(
reference, _id_available, jni.JniCallType.intType, []).integer;
}

static final _id_close =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"close", r"()V");

/// from: public void close()
void close() {
return jni.Jni.accessors.callMethodWithArgs(
reference, _id_close, jni.JniCallType.voidType, []).check();
}

static final _id_mark =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"mark", r"(I)V");

/// from: public void mark(int i)
void mark(
int i,
) {
return jni.Jni.accessors.callMethodWithArgs(reference, _id_mark,
jni.JniCallType.voidType, [jni.JValueInt(i)]).check();
}

static final _id_reset =
jni.Jni.accessors.getMethodIDOf(_class.reference, r"reset", r"()V");

/// from: public void reset()
void reset() {
return jni.Jni.accessors.callMethodWithArgs(
reference, _id_reset, jni.JniCallType.voidType, []).check();
}

static final _id_markSupported = jni.Jni.accessors
.getMethodIDOf(_class.reference, r"markSupported", r"()Z");

/// from: public boolean markSupported()
bool markSupported() {
return jni.Jni.accessors.callMethodWithArgs(
reference, _id_markSupported, jni.JniCallType.booleanType, []).boolean;
}

static final _id_transferTo = jni.Jni.accessors.getMethodIDOf(
_class.reference, r"transferTo", r"(Ljava/io/OutputStream;)J");

/// from: public long transferTo(java.io.OutputStream outputStream)
int transferTo(
jni.JObject outputStream,
) {
return jni.Jni.accessors.callMethodWithArgs(reference, _id_transferTo,
jni.JniCallType.longType, [outputStream.reference]).long;
}
}

class $InputStreamType extends jni.JObjType<InputStream> {
const $InputStreamType();

@override
String get signature => r"Ljava/io/InputStream;";

@override
InputStream fromRef(jni.JObjectPtr ref) => InputStream.fromRef(ref);

@override
jni.JObjType get superType => const jni.JObjectType();

@override
final superCount = 1;

@override
int get hashCode => ($InputStreamType).hashCode;

@override
bool operator ==(Object other) {
return other.runtimeType == ($InputStreamType) && other is $InputStreamType;
}
}
1 change: 1 addition & 0 deletions pkgs/java_http/lib/src/third_party/java/io/_package.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export "InputStream.dart";
Loading

0 comments on commit db276f8

Please sign in to comment.