diff --git a/build.gradle b/build.gradle index a18c3f0..c67023d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,143 +1,144 @@ -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'net.sf.proguard:proguard-gradle:6.0.1' - } -} - -plugins { - id "java" - id "com.github.johnrengelman.shadow" version "2.0.2" - id "org.sonarqube" version "2.6.2" -} - -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar -import proguard.gradle.ProGuardTask - - -sourceCompatibility = '1.8' -[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' - -if (!hasProperty('mainClass')) { - ext.mainClass = 'com.annimon.ownlang.Main' -} - -ext.generatedJavaDir = "${rootProject.projectDir}/src/main/generatedJava" -sourceSets.main.java.srcDirs += project.generatedJavaDir - -repositories { - jcenter() -} - -task generateJavaSources() { - doLast { - def source = """ - package com.annimon.ownlang; - class Gen { - private Gen() {} - public static final String BUILD_DATE = "${new Date().format('YYMMdd')}"; - } - """ - def genFile = new File("${project.generatedJavaDir}/com/annimon/ownlang/Gen.java") - genFile.getParentFile().mkdirs() - genFile.write(source) - } -} -compileJava.dependsOn(generateJavaSources) - -task run(dependsOn: classes, type: JavaExec) { - main = project.mainClass - classpath = sourceSets.main.runtimeClasspath - standardInput = System.in - ignoreExitValue = true -} - -task runOptimizing(dependsOn: classes, type: JavaExec) { - main = project.mainClass - classpath = sourceSets.main.runtimeClasspath - ignoreExitValue = true - // args '-o 9 -m -a -f examples/game/minesweeper.own'.split(' ') - args '-o 9 -m -a -f program.own'.split(' ') -} - -task dist(type: ShadowJar) { - from sourceSets.main.output - configurations = [project.configurations.runtime] - destinationDir file("$rootProject.projectDir/dist") - - exclude 'META-INF/*.DSA' - exclude 'META-INF/*.RSA' - exclude 'META-INF/maven/**' - exclude 'LICENSE*' - - manifest.attributes( - 'Main-Class': project.mainClass, - 'Build-Date': new Date().format('YYMMdd') - ) -} - -task proguard(dependsOn: dist, type: ProGuardTask) { - configuration "$rootProject.projectDir/proguard.properties" - injars "$rootProject.projectDir/dist/OwnLang.jar" - outjars "$rootProject.projectDir/store/OwnLang.jar" - - // Automatically handle the Java version of this build. - if (System.getProperty('java.version').startsWith('1.')) { - // Before Java 9, the runtime classes were packaged in a single jar file. - libraryjars "${System.getProperty('java.home')}/lib/rt.jar" - } else { - // As of Java 9, the runtime classes are packaged in modular jmod files. - def jmods = files { file("${System.getProperty('java.home')}/jmods").listFiles() } - jmods.each { - libraryjars it, jarfilter: '!**.jar', filter: '!module-info.class' - } - } -} - -task sandbox(dependsOn: proguard, type: Jar) { - from zipTree("$rootProject.projectDir/store/OwnLang.jar") - libsDirName = "$rootProject.projectDir/store" - appendix = "Sandbox" - - exclude "**/modules/canvas/**", "**/modules/canvasfx/**", "**/modules/forms/**", - "**/modules/java/**", "**/modules/jdbc/**", "**/modules/robot/**", - "**/modules/socket/**", "io/**", - "**/modules/aimp/**", "aimpremote/**", - "**/modules/downloader/**", - "**/modules/zip/**", "**/modules/gzip/**", - "jline/**", "org/fusesource/**", "META-INF/native/**" - - manifest { - attributes 'Main-Class': project.mainClass - } -} - -dependencies { - compile ('io.socket:socket.io-client:1.0.0') { - exclude group: 'org.json', module: 'json' - } - compile 'org.json:json:20180130' - compile 'org.yaml:snakeyaml:1.20' - compile 'jline:jline:2.14.5' - - testImplementation 'junit:junit:4.12' - testImplementation 'org.openjdk.jmh:jmh-core:1.13' - testImplementation 'org.openjdk.jmh:jmh-generator-annprocess:1.13' -} - -sonarqube { - properties { - property "sonar.projectName", "Own-Programming-Language-Tutorial" - property "sonar.projectKey", "aNNiMON_Own-Programming-Language-Tutorial" - property "sonar.host.url", "https://sonarcloud.io" - } -} - -test { - testLogging { - events "passed", "skipped", "failed" - exceptionFormat "full" - } +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'net.sf.proguard:proguard-gradle:6.0.1' + } +} + +plugins { + id "java" + id "com.github.johnrengelman.shadow" version "2.0.2" + id "org.sonarqube" version "2.6.2" +} + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import proguard.gradle.ProGuardTask + + +sourceCompatibility = '1.8' +[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + +if (!hasProperty('mainClass')) { + ext.mainClass = 'com.annimon.ownlang.Main' +} + +ext.generatedJavaDir = "${rootProject.projectDir}/src/main/generatedJava" +sourceSets.main.java.srcDirs += project.generatedJavaDir + +repositories { + jcenter() +} + +task generateJavaSources() { + doLast { + def source = """ + package com.annimon.ownlang; + class Gen { + private Gen() {} + public static final String BUILD_DATE = "${new Date().format('YYMMdd')}"; + } + """ + def genFile = new File("${project.generatedJavaDir}/com/annimon/ownlang/Gen.java") + genFile.getParentFile().mkdirs() + genFile.write(source) + } +} +compileJava.dependsOn(generateJavaSources) + +task run(dependsOn: classes, type: JavaExec) { + main = project.mainClass + classpath = sourceSets.main.runtimeClasspath + standardInput = System.in + ignoreExitValue = true +} + +task runOptimizing(dependsOn: classes, type: JavaExec) { + main = project.mainClass + classpath = sourceSets.main.runtimeClasspath + ignoreExitValue = true + // args '-o 9 -m -a -f examples/game/minesweeper.own'.split(' ') + args '-o 9 -m -a -f program.own'.split(' ') +} + +task dist(type: ShadowJar) { + from sourceSets.main.output + configurations = [project.configurations.runtime] + destinationDir file("$rootProject.projectDir/dist") + + exclude 'META-INF/*.DSA' + exclude 'META-INF/*.RSA' + exclude 'META-INF/maven/**' + exclude 'LICENSE*' + + manifest.attributes( + 'Main-Class': project.mainClass, + 'Build-Date': new Date().format('YYMMdd') + ) +} + +task proguard(dependsOn: dist, type: ProGuardTask) { + configuration "$rootProject.projectDir/proguard.properties" + injars "$rootProject.projectDir/dist/OwnLang.jar" + outjars "$rootProject.projectDir/store/OwnLang.jar" + + // Automatically handle the Java version of this build. + if (System.getProperty('java.version').startsWith('1.')) { + // Before Java 9, the runtime classes were packaged in a single jar file. + libraryjars "${System.getProperty('java.home')}/lib/rt.jar" + } else { + // As of Java 9, the runtime classes are packaged in modular jmod files. + def jmods = files { file("${System.getProperty('java.home')}/jmods").listFiles() } + jmods.each { + libraryjars it, jarfilter: '!**.jar', filter: '!module-info.class' + } + } +} + +task sandbox(dependsOn: proguard, type: Jar) { + from zipTree("$rootProject.projectDir/store/OwnLang.jar") + libsDirName = "$rootProject.projectDir/store" + appendix = "Sandbox" + + exclude "**/modules/canvas/**", "**/modules/canvasfx/**", "**/modules/forms/**", + "**/modules/java/**", "**/modules/jdbc/**", "**/modules/robot/**", + "**/modules/okhttp/**", + "**/modules/socket/**", "io/**", + "**/modules/aimp/**", "aimpremote/**", + "**/modules/downloader/**", + "**/modules/zip/**", "**/modules/gzip/**", + "jline/**", "org/fusesource/**", "META-INF/native/**" + + manifest { + attributes 'Main-Class': project.mainClass + } +} + +dependencies { + compile ('io.socket:socket.io-client:1.0.0') { + exclude group: 'org.json', module: 'json' + } + compile 'org.json:json:20180130' + compile 'org.yaml:snakeyaml:1.20' + compile 'jline:jline:2.14.5' + + testImplementation 'junit:junit:4.12' + testImplementation 'org.openjdk.jmh:jmh-core:1.13' + testImplementation 'org.openjdk.jmh:jmh-generator-annprocess:1.13' +} + +sonarqube { + properties { + property "sonar.projectName", "Own-Programming-Language-Tutorial" + property "sonar.projectKey", "aNNiMON_Own-Programming-Language-Tutorial" + property "sonar.host.url", "https://sonarcloud.io" + } +} + +test { + testLogging { + events "passed", "skipped", "failed" + exceptionFormat "full" + } } \ No newline at end of file diff --git a/examples/network/okhttp_imgur_upload.own b/examples/network/okhttp_imgur_upload.own new file mode 100644 index 0000000..d9ea0a4 --- /dev/null +++ b/examples/network/okhttp_imgur_upload.own @@ -0,0 +1,17 @@ +use ["std", "okhttp"] + +// https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/PostMultipart.java + +println okhttp.request() + .header("Authorization", "Client-ID 9199fdef135c122") + .url("https://api.imgur.com/3/image") + .post(MultipartBody.builder() + .setType(MultipartBody.FORM) + .addFormDataPart("title", "Sample image") + .addFormDataPart("image", "image.png", RequestBody.file("image/png", "image.png")) + .build() + ) + .newCall(okhttp.client) + .execute() + .body() + .string() \ No newline at end of file diff --git a/examples/network/okhttp_telegram_sendvoice.own b/examples/network/okhttp_telegram_sendvoice.own new file mode 100644 index 0000000..1070ab7 --- /dev/null +++ b/examples/network/okhttp_telegram_sendvoice.own @@ -0,0 +1,20 @@ +use ["std", "okhttp"] + +TOKEN = "your bot token" + +println okhttp.request() + .url("https://api.telegram.org/bot" + TOKEN + "/sendVoice") + .post(MultipartBody.builder() + .setType(MultipartBody.FORM) + .addFormData({ + "chat_id": "-1001100000123", + "caption": "sample text" + }) + .addFormDataPart("voice", "file.ogg", + RequestBody.file("audio/ogg", "voice.ogg")) + .build() + ) + .newCall(okhttp.client) + .execute() + .body() + .string() \ No newline at end of file diff --git a/examples/network/okhttp_websocket.own b/examples/network/okhttp_websocket.own new file mode 100644 index 0000000..645260f --- /dev/null +++ b/examples/network/okhttp_websocket.own @@ -0,0 +1,20 @@ +use ["std", "okhttp"] + +// https://github.com/square/okhttp/blob/b21ed68c08c2a5c1eb0bbe93a6f720d1aa2820da/samples/guide/src/main/java/okhttp3/recipes/WebSocketEcho.java + +okhttp.client.newWebSocket( + okhttp.request().url("ws://echo.websocket.org"), + { + "onOpen": def(ws, resp) { + ws.send("Hello...") + ws.send("...World!") + ws.close(1000, "Goodbye, World!") + }, + "onTextMessage": def(ws, text) = echo(text), + "onBytesMessage": def(ws, bytes) = echo(bytes), + "onClosing": def(ws, code, reason) { + ws.close(1000) + echo("CLOSE:", code, reason) + } + } +) \ No newline at end of file diff --git a/src/main/java/com/annimon/ownlang/lib/ValueUtils.java b/src/main/java/com/annimon/ownlang/lib/ValueUtils.java index de5803d..016315c 100644 --- a/src/main/java/com/annimon/ownlang/lib/ValueUtils.java +++ b/src/main/java/com/annimon/ownlang/lib/ValueUtils.java @@ -1,129 +1,133 @@ -package com.annimon.ownlang.lib; - -import com.annimon.ownlang.exceptions.TypeException; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.Iterator; -import java.util.Map; -import org.json.JSONArray; -import org.json.JSONObject; - -public final class ValueUtils { - - private ValueUtils() { } - - public static Object toObject(Value val) { - switch (val.type()) { - case Types.ARRAY: - return toObject((ArrayValue) val); - case Types.MAP: - return toObject((MapValue) val); - case Types.NUMBER: - return val.raw(); - case Types.STRING: - return val.asString(); - default: - return JSONObject.NULL; - } - } - - public static Object toObject(MapValue map) { - final JSONObject result = new JSONObject(); - for (Map.Entry entry : map) { - final String key = entry.getKey().asString(); - final Object value = toObject(entry.getValue()); - result.put(key, value); - } - return result; - } - - public static Object toObject(ArrayValue array) { - final JSONArray result = new JSONArray(); - for (Value value : array) { - result.put(toObject(value)); - } - return result; - } - - public static Value toValue(Object obj) { - if (obj instanceof JSONObject) { - return toValue((JSONObject) obj); - } - if (obj instanceof JSONArray) { - return toValue((JSONArray) obj); - } - if (obj instanceof String) { - return new StringValue((String) obj); - } - if (obj instanceof Number) { - return NumberValue.of(((Number) obj)); - } - if (obj instanceof Boolean) { - return NumberValue.fromBoolean((Boolean) obj); - } - // NULL or other - return NumberValue.ZERO; - } - - public static MapValue toValue(JSONObject json) { - final MapValue result = new MapValue(json.length()); - final Iterator it = json.keys(); - while(it.hasNext()) { - final String key = it.next(); - final Value value = toValue(json.get(key)); - result.set(new StringValue(key), value); - } - return result; - } - - public static ArrayValue toValue(JSONArray json) { - final int length = json.length(); - final ArrayValue result = new ArrayValue(length); - for (int i = 0; i < length; i++) { - final Value value = toValue(json.get(i)); - result.set(i, value); - } - return result; - } - - public static Number getNumber(Value value) { - if (value.type() == Types.NUMBER) return ((NumberValue) value).raw(); - return value.asInt(); - } - - public static float getFloatNumber(Value value) { - if (value.type() == Types.NUMBER) return ((NumberValue) value).raw().floatValue(); - return (float) value.asNumber(); - } - - public static byte[] toByteArray(ArrayValue array) { - final int size = array.size(); - final byte[] result = new byte[size]; - for (int i = 0; i < size; i++) { - result[i] = (byte) array.get(i).asInt(); - } - return result; - } - - public static Function consumeFunction(Value value, int argumentNumber) { - final int type = value.type(); - if (type != Types.FUNCTION) { - throw new TypeException("Function expected at argument " + (argumentNumber + 1) - + ", but found " + Types.typeToString(type)); - } - return ((FunctionValue) value).getValue(); - } - - public static MapValue collectNumberConstants(Class clazz, Class type) { - MapValue result = new MapValue(20); - for (Field field : clazz.getDeclaredFields()) { - if (!Modifier.isStatic(field.getModifiers())) continue; - if (!field.getType().equals(type)) continue; - try { - result.set(field.getName(), NumberValue.of((T) field.get(type))); - } catch (IllegalAccessException ignore) { - } - } - return result; - } -} +package com.annimon.ownlang.lib; + +import com.annimon.ownlang.exceptions.TypeException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Iterator; +import java.util.Map; +import org.json.JSONArray; +import org.json.JSONObject; + +public final class ValueUtils { + + private ValueUtils() { } + + public static Object toObject(Value val) { + switch (val.type()) { + case Types.ARRAY: + return toObject((ArrayValue) val); + case Types.MAP: + return toObject((MapValue) val); + case Types.NUMBER: + return val.raw(); + case Types.STRING: + return val.asString(); + default: + return JSONObject.NULL; + } + } + + public static Object toObject(MapValue map) { + final JSONObject result = new JSONObject(); + for (Map.Entry entry : map) { + final String key = entry.getKey().asString(); + final Object value = toObject(entry.getValue()); + result.put(key, value); + } + return result; + } + + public static Object toObject(ArrayValue array) { + final JSONArray result = new JSONArray(); + for (Value value : array) { + result.put(toObject(value)); + } + return result; + } + + public static Value toValue(Object obj) { + if (obj instanceof JSONObject) { + return toValue((JSONObject) obj); + } + if (obj instanceof JSONArray) { + return toValue((JSONArray) obj); + } + if (obj instanceof String) { + return new StringValue((String) obj); + } + if (obj instanceof Number) { + return NumberValue.of(((Number) obj)); + } + if (obj instanceof Boolean) { + return NumberValue.fromBoolean((Boolean) obj); + } + // NULL or other + return NumberValue.ZERO; + } + + public static MapValue toValue(JSONObject json) { + final MapValue result = new MapValue(json.length()); + final Iterator it = json.keys(); + while(it.hasNext()) { + final String key = it.next(); + final Value value = toValue(json.get(key)); + result.set(new StringValue(key), value); + } + return result; + } + + public static ArrayValue toValue(JSONArray json) { + final int length = json.length(); + final ArrayValue result = new ArrayValue(length); + for (int i = 0; i < length; i++) { + final Value value = toValue(json.get(i)); + result.set(i, value); + } + return result; + } + + public static Number getNumber(Value value) { + if (value.type() == Types.NUMBER) return ((NumberValue) value).raw(); + return value.asInt(); + } + + public static float getFloatNumber(Value value) { + if (value.type() == Types.NUMBER) return ((NumberValue) value).raw().floatValue(); + return (float) value.asNumber(); + } + + public static byte[] toByteArray(ArrayValue array) { + final int size = array.size(); + final byte[] result = new byte[size]; + for (int i = 0; i < size; i++) { + result[i] = (byte) array.get(i).asInt(); + } + return result; + } + + public static Function consumeFunction(Value value, int argumentNumber) { + return consumeFunction(value, " at argument " + (argumentNumber + 1)); + } + + public static Function consumeFunction(Value value, String errorMessage) { + final int type = value.type(); + if (type != Types.FUNCTION) { + throw new TypeException("Function expected" + errorMessage + + ", but found " + Types.typeToString(type)); + } + return ((FunctionValue) value).getValue(); + } + + public static MapValue collectNumberConstants(Class clazz, Class type) { + MapValue result = new MapValue(20); + for (Field field : clazz.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers())) continue; + if (!field.getType().equals(type)) continue; + try { + result.set(field.getName(), NumberValue.of((T) field.get(type))); + } catch (IllegalAccessException ignore) { + } + } + return result; + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/CallValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/CallValue.java new file mode 100644 index 0000000..41db95a --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/CallValue.java @@ -0,0 +1,62 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.lib.Arguments; +import com.annimon.ownlang.lib.Converters; +import com.annimon.ownlang.lib.Function; +import com.annimon.ownlang.lib.MapValue; +import com.annimon.ownlang.lib.NumberValue; +import com.annimon.ownlang.lib.StringValue; +import com.annimon.ownlang.lib.Value; +import com.annimon.ownlang.lib.ValueUtils; +import java.io.IOException; +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Response; + +public class CallValue extends MapValue { + + private final Call call; + + public CallValue(Call call) { + super(6); + this.call = call; + init(); + } + + private void init() { + set("cancel", Converters.voidToVoid(call::cancel)); + set("enqueue", this::enqueue); + set("execute", this::execute); + set("isCanceled", Converters.voidToBoolean(call::isCanceled)); + set("isExecuted", Converters.voidToBoolean(call::isExecuted)); + } + + private Value enqueue(Value[] args) { + Arguments.checkOrOr(1, 2, args.length); + final Function onResponse = ValueUtils.consumeFunction(args[0], 0); + call.enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) throws IOException { + onResponse.execute(new CallValue(call), new ResponseValue(response)); + } + + @Override + public void onFailure(Call call, IOException e) { + if (args.length == 2) { + ValueUtils.consumeFunction(args[1], 1) + .execute(new CallValue(call), new StringValue(e.getMessage())); + } + } + }); + return NumberValue.ZERO; + } + + private Value execute(Value[] args) { + Arguments.check(0, args.length); + try { + return new ResponseValue(call.execute()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/HttpClientValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/HttpClientValue.java new file mode 100644 index 0000000..b222339 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/HttpClientValue.java @@ -0,0 +1,122 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.exceptions.TypeException; +import com.annimon.ownlang.lib.*; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import okio.ByteString; + +public class HttpClientValue extends MapValue { + + private final OkHttpClient client; + + public HttpClientValue(OkHttpClient client) { + super(10); + this.client = client; + init(); + } + + public OkHttpClient getClient() { + return client; + } + + private void init() { + set("connectTimeoutMillis", Converters.voidToInt(client::connectTimeoutMillis)); + set("followRedirects", Converters.voidToBoolean(client::followRedirects)); + set("followSslRedirects", Converters.voidToBoolean(client::followSslRedirects)); + set("newCall", args -> { + Arguments.check(1, args.length); + final Request request = Values.getRequest(args[0], " at first argument"); + return new CallValue(client.newCall(request)); + }); + set("newWebSocket", this::newWebSocket); + set("pingIntervalMillis", Converters.voidToInt(client::pingIntervalMillis)); + set("readTimeoutMillis", Converters.voidToInt(client::readTimeoutMillis)); + set("retryOnConnectionFailure", Converters.voidToBoolean(client::retryOnConnectionFailure)); + set("writeTimeoutMillis", Converters.voidToInt(client::writeTimeoutMillis)); + } + + private static final StringValue onOpen = new StringValue("onOpen"); + private static final StringValue onTextMessage = new StringValue("onTextMessage"); + private static final StringValue onBytesMessage = new StringValue("onBytesMessage"); + private static final StringValue onClosing = new StringValue("onClosing"); + private static final StringValue onClosed = new StringValue("onClosed"); + private static final StringValue onFailure = new StringValue("onFailure"); + + private Value newWebSocket(Value[] args) { + Arguments.check(2, args.length); + final Request request = Values.getRequest(args[0], " at first argument"); + if (args[1].type() != Types.MAP) { + throw new TypeException("Map expected at second argument"); + } + final MapValue callbacks = (MapValue) args[1]; + final WebSocket ws = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + final Value func = callbacks.get(onOpen); + if (func != null) { + ValueUtils.consumeFunction(func, " at onOpen").execute( + new WebSocketValue(webSocket), + new ResponseValue(response)); + } + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + final Value func = callbacks.get(onTextMessage); + if (func != null) { + ValueUtils.consumeFunction(func, "at onTextMessage").execute( + new WebSocketValue(webSocket), + new StringValue(text)); + } + } + + @Override + public void onMessage(WebSocket webSocket, ByteString bytes) { + final Value func = callbacks.get(onBytesMessage); + if (func != null) { + ValueUtils.consumeFunction(func, "at onBytesMessage").execute( + new WebSocketValue(webSocket), + ArrayValue.of(bytes.toByteArray())); + } + } + + @Override + public void onClosing(WebSocket webSocket, int code, String reason) { + final Value func = callbacks.get(onClosing); + if (func != null) { + ValueUtils.consumeFunction(func, "at onClosing").execute( + new WebSocketValue(webSocket), + NumberValue.of(code), + new StringValue(reason)); + } + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + final Value func = callbacks.get(onClosed); + if (func != null) { + ValueUtils.consumeFunction(func, "at onClosed").execute( + new WebSocketValue(webSocket), + NumberValue.of(code), + new StringValue(reason)); + } + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + final Value func = callbacks.get(onFailure); + if (func != null) { + ValueUtils.consumeFunction(func, "at onFailure").execute( + new WebSocketValue(webSocket), + new StringValue(t.getMessage()), + new ResponseValue(response)); + } + } + }); + return new CallValue(client.newCall(request)); + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/MultipartBodyBuilderValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/MultipartBodyBuilderValue.java new file mode 100644 index 0000000..b7d891e --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/MultipartBodyBuilderValue.java @@ -0,0 +1,70 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.exceptions.TypeException; +import com.annimon.ownlang.lib.Arguments; +import com.annimon.ownlang.lib.MapValue; +import com.annimon.ownlang.lib.Types; +import com.annimon.ownlang.lib.Value; +import java.util.Map; +import okhttp3.MediaType; +import okhttp3.MultipartBody; + +public class MultipartBodyBuilderValue extends MapValue { + + private final MultipartBody.Builder builder; + + public MultipartBodyBuilderValue() { + super(5); + this.builder = new MultipartBody.Builder(); + init(); + } + + private void init() { + set("addFormData", this::addFormData); + set("addFormDataPart", this::addFormDataPart); + set("addPart", this::addPart); + set("build", args -> new MultipartBodyValue(builder.build())); + set("setType", args -> { + Arguments.check(1, args.length); + builder.setType(MediaType.parse(args[0].asString())); + return this; + }); + } + + private Value addFormDataPart(Value[] args) { + Arguments.checkOrOr(2, 3, args.length); + if (args.length == 2) { + builder.addFormDataPart(args[0].asString(), args[1].asString()); + } else { + builder.addFormDataPart( + args[0].asString(), + args[1].asString(), + Values.getRequestBody(args[2], " at third argument")); + } + return this; + } + + private Value addFormData(Value[] args) { + Arguments.check(1, args.length); + if (args[0].type() != Types.MAP) { + throw new TypeException("Map expected at first argument"); + } + for (Map.Entry entry : ((MapValue) args[0])) { + builder.addFormDataPart(entry.getKey().asString(), entry.getValue().asString()); + } + return this; + } + + private Value addPart(Value[] args) { + Arguments.checkOrOr(2, 3, args.length); + if (args.length == 1) { + builder.addPart( + Values.getRequestBody(args[0], " at first argument")); + } else { + builder.addPart( + Values.getHeaders(args[0], " at first argument"), + Values.getRequestBody(args[1], " at second argument")); + } + return this; + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/MultipartBodyValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/MultipartBodyValue.java new file mode 100644 index 0000000..41db158 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/MultipartBodyValue.java @@ -0,0 +1,24 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.lib.Converters; +import okhttp3.MultipartBody; + +public class MultipartBodyValue extends RequestBodyValue { + + private final MultipartBody multipartBody; + + public MultipartBodyValue(MultipartBody multipartBody) { + super(multipartBody, 5); + this.multipartBody = multipartBody; + init(); + } + + public MultipartBody getMultipartBody() { + return multipartBody; + } + + private void init() { + set("boundary", Converters.voidToString(multipartBody::boundary)); + set("size", Converters.voidToInt(multipartBody::size)); + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/RequestBodyValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/RequestBodyValue.java new file mode 100644 index 0000000..b9ea590 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/RequestBodyValue.java @@ -0,0 +1,55 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.lib.Arguments; +import com.annimon.ownlang.lib.Converters; +import com.annimon.ownlang.lib.MapValue; +import com.annimon.ownlang.lib.StringValue; +import java.io.IOException; +import java.nio.charset.Charset; +import okhttp3.MediaType; +import okhttp3.RequestBody; + +public class RequestBodyValue extends MapValue { + + private final RequestBody requestBody; + private final MediaType mediaType; + + public RequestBodyValue(RequestBody requestBody) { + this(requestBody, 0); + } + + protected RequestBodyValue(RequestBody requestBody, int methodsCount) { + super(4 + methodsCount); + this.requestBody = requestBody; + this.mediaType = requestBody.contentType(); + init(); + } + + public RequestBody getRequestBody() { + return requestBody; + } + + public MediaType getMediaType() { + return mediaType; + } + + private void init() { + set("getContentLength", Converters.voidToLong(() -> { + try { + return requestBody.contentLength(); + } catch (IOException ex) { + return -1; + } + })); + set("getType", Converters.voidToString(mediaType::type)); + set("getSubtype", Converters.voidToString(mediaType::subtype)); + set("getCharset", args -> { + Arguments.checkOrOr(0, 1, args.length); + if (args.length == 0) { + return new StringValue(mediaType.charset().name()); + } else { + return new StringValue(mediaType.charset(Charset.forName(args[0].asString())).name()); + } + }); + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/RequestBuilderValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/RequestBuilderValue.java new file mode 100644 index 0000000..51d4edf --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/RequestBuilderValue.java @@ -0,0 +1,109 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.lib.Arguments; +import com.annimon.ownlang.lib.Converters.VoidToVoidFunction; +import com.annimon.ownlang.lib.Function; +import com.annimon.ownlang.lib.MapValue; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; + +public class RequestBuilderValue extends MapValue { + + private final Request.Builder requestBuilder; + + public RequestBuilderValue() { + super(15); + requestBuilder = new Request.Builder(); + init(); + } + + public Request getRequest() { + return requestBuilder.build(); + } + + private void init() { + set("addHeader", args -> { + Arguments.check(2, args.length); + requestBuilder.addHeader(args[0].asString(), args[1].asString()); + return this; + }); + set("cacheControl", args -> { + Arguments.check(1, args.length); + // TODO + return this; + }); + set("delete", httpMethod(requestBuilder::delete, requestBuilder::delete)); + set("get", args -> { + requestBuilder.get(); + return this; + }); + set("head", args -> { + requestBuilder.head(); + return this; + }); + set("header", args -> { + Arguments.check(2, args.length); + requestBuilder.header(args[0].asString(), args[1].asString()); + return this; + }); + set("headers", args -> { + Arguments.check(1, args.length); + requestBuilder.headers(Values.getHeaders(args[0], " at first argument")); + return this; + }); + set("method", args -> { + Arguments.checkOrOr(1, 2, args.length); + final RequestBody body; + if (args.length == 1) { + body = null; + } else { + body = Values.getRequestBody(args[1], " at second argument"); + } + requestBuilder.method(args[0].asString(), body); + return this; + }); + set("newCall", args -> { + Arguments.check(1, args.length); + final OkHttpClient client = Values.getHttpClient(args[0], " at first argument"); + return new CallValue(client.newCall(getRequest())); + }); + set("patch", httpMethod(requestBuilder::patch)); + set("post", httpMethod(requestBuilder::post)); + set("put", httpMethod(requestBuilder::put)); + set("removeHeader", args -> { + Arguments.check(1, args.length); + requestBuilder.removeHeader(args[0].asString()); + return this; + }); + set("url", args -> { + Arguments.check(1, args.length); + requestBuilder.url(args[0].asString()); + return this; + }); + } + + private Function httpMethod(VoidToVoidFunction voidFunc, RequestBodyToVoidFunction bodyFunc) { + return (args) -> { + Arguments.checkOrOr(0, 1, args.length); + if (args.length == 0) { + voidFunc.apply(); + } else { + bodyFunc.apply(Values.getRequestBody(args[0], " at first argument")); + } + return this; + }; + } + + private Function httpMethod(RequestBodyToVoidFunction bodyFunc) { + return (args) -> { + Arguments.check(1, args.length); + bodyFunc.apply(Values.getRequestBody(args[0], " at first argument")); + return this; + }; + } + + private interface RequestBodyToVoidFunction { + void apply(RequestBody value); + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/ResponseBodyValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/ResponseBodyValue.java new file mode 100644 index 0000000..faa0845 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/ResponseBodyValue.java @@ -0,0 +1,56 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.Console; +import com.annimon.ownlang.lib.Arguments; +import com.annimon.ownlang.lib.ArrayValue; +import com.annimon.ownlang.lib.Converters; +import com.annimon.ownlang.lib.MapValue; +import com.annimon.ownlang.lib.NumberValue; +import com.annimon.ownlang.lib.StringValue; +import java.io.IOException; +import okhttp3.ResponseBody; +import okio.BufferedSink; +import okio.Okio; + +public class ResponseBodyValue extends MapValue { + + private final ResponseBody responseBody; + + public ResponseBodyValue(ResponseBody response) { + super(8); + this.responseBody = response; + init(); + } + + private void init() { + set("bytes", args -> { + try { + return ArrayValue.of(responseBody.bytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + set("close", Converters.voidToVoid(responseBody::close)); + set("contentLength", Converters.voidToLong(responseBody::contentLength)); + set("contentType", args -> new StringValue(responseBody.contentType().toString())); + set("string", args -> { + try { + return new StringValue(responseBody.string()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + set("file", args -> { + Arguments.check(1, args.length); + try { + BufferedSink sink = Okio.buffer(Okio.sink(Console.fileInstance(args[0].asString()))); + sink.writeAll(responseBody.source()); + sink.close(); + return NumberValue.ONE; + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/ResponseValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/ResponseValue.java new file mode 100644 index 0000000..aee83fb --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/ResponseValue.java @@ -0,0 +1,52 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.lib.Arguments; +import com.annimon.ownlang.lib.ArrayValue; +import com.annimon.ownlang.lib.Converters; +import com.annimon.ownlang.lib.MapValue; +import com.annimon.ownlang.lib.StringValue; +import java.util.List; +import java.util.Map; +import okhttp3.Response; + +public class ResponseValue extends MapValue { + + private final Response response; + + public ResponseValue(Response response) { + super(15); + this.response = response; + init(); + } + + private void init() { + set("body", args -> new ResponseBodyValue(response.body())); + set("cacheResponse", args -> new ResponseValue(response.cacheResponse())); + set("code", Converters.voidToInt(response::code)); + set("close", Converters.voidToVoid(response::close)); + set("header", args -> { + Arguments.checkOrOr(1, 2, args.length); + final String defaultValue = (args.length == 1) ? null : args[1].asString(); + return new StringValue(response.header(args[0].asString(), defaultValue)); + }); + set("headers", args -> { + Arguments.checkOrOr(0, 1, args.length); + if (args.length == 0) { + final Map> headers = response.headers().toMultimap(); + final MapValue result = new MapValue(headers.size()); + for (Map.Entry> entry : headers.entrySet()) { + result.set(entry.getKey(), ArrayValue.of(entry.getValue().toArray(new String[0]))); + } + return result; + } else { + return ArrayValue.of(response.headers(args[0].asString()).toArray(new String[0])); + } + }); + set("message", Converters.voidToString(response::message)); + set("networkResponse", args -> new ResponseValue(response.networkResponse())); + set("priorResponse", args -> new ResponseValue(response.priorResponse())); + set("receivedResponseAtMillis", Converters.voidToLong(response::receivedResponseAtMillis)); + set("sentRequestAtMillis", Converters.voidToLong(response::sentRequestAtMillis)); + } + +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/Values.java b/src/main/java/com/annimon/ownlang/modules/okhttp/Values.java new file mode 100644 index 0000000..eb83ec0 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/Values.java @@ -0,0 +1,47 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.exceptions.TypeException; +import com.annimon.ownlang.lib.MapValue; +import com.annimon.ownlang.lib.Types; +import com.annimon.ownlang.lib.Value; +import java.util.LinkedHashMap; +import java.util.Map; +import okhttp3.Headers; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; + +public class Values { + + public static RequestBody getRequestBody(Value arg, String msg) { + if (arg.type() == Types.MAP && (arg instanceof RequestBodyValue)) { + return ((RequestBodyValue) arg).getRequestBody(); + } + throw new TypeException("RequestBody value expected" + msg); + } + + public static Request getRequest(Value arg, String msg) { + if (arg.type() == Types.MAP && (arg instanceof RequestBuilderValue)) { + return ((RequestBuilderValue) arg).getRequest(); + } + throw new TypeException("Request value expected" + msg); + } + + public static OkHttpClient getHttpClient(Value arg, String msg) { + if (arg.type() == Types.MAP && (arg instanceof HttpClientValue)) { + return ((HttpClientValue) arg).getClient(); + } + throw new TypeException("HttpClient value expected" + msg); + } + + public static Headers getHeaders(Value arg, String msg) { + if (arg.type() != Types.MAP) { + throw new TypeException("Map expected" + msg); + } + final Map headers = new LinkedHashMap<>(); + for (Map.Entry entry : ((MapValue) arg)) { + headers.put(entry.getKey().asString(), entry.getValue().asString()); + } + return Headers.of(headers); + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/WebSocketValue.java b/src/main/java/com/annimon/ownlang/modules/okhttp/WebSocketValue.java new file mode 100644 index 0000000..9af00d0 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/WebSocketValue.java @@ -0,0 +1,46 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.lib.Arguments; +import com.annimon.ownlang.lib.ArrayValue; +import com.annimon.ownlang.lib.Converters; +import com.annimon.ownlang.lib.MapValue; +import com.annimon.ownlang.lib.NumberValue; +import com.annimon.ownlang.lib.Types; +import com.annimon.ownlang.lib.ValueUtils; +import okhttp3.WebSocket; +import okio.ByteString; + +public class WebSocketValue extends MapValue { + + private final WebSocket ws; + + protected WebSocketValue(WebSocket ws) { + super(4); + this.ws = ws; + init(); + } + + public WebSocket getWebSocket() { + return ws; + } + + private void init() { + set("cancel", Converters.voidToVoid(ws::cancel)); + set("close", args -> { + Arguments.checkOrOr(1, 2, args.length); + final String reason = (args.length == 2) ? args[1].asString() : null; + return NumberValue.fromBoolean(ws.close(args[0].asInt(), reason)); + }); + set("queueSize", Converters.voidToLong(ws::queueSize)); + set("send", args -> { + Arguments.check(1, args.length); + final boolean result; + if (args[0].type() == Types.ARRAY) { + result = ws.send(ByteString.of( ValueUtils.toByteArray(((ArrayValue) args[0])) )); + } else { + result = ws.send(args[0].asString()); + } + return NumberValue.fromBoolean(result); + }); + } +} diff --git a/src/main/java/com/annimon/ownlang/modules/okhttp/okhttp.java b/src/main/java/com/annimon/ownlang/modules/okhttp/okhttp.java new file mode 100644 index 0000000..cf65b18 --- /dev/null +++ b/src/main/java/com/annimon/ownlang/modules/okhttp/okhttp.java @@ -0,0 +1,81 @@ +package com.annimon.ownlang.modules.okhttp; + +import com.annimon.ownlang.Console; +import com.annimon.ownlang.exceptions.TypeException; +import com.annimon.ownlang.lib.Arguments; +import com.annimon.ownlang.lib.ArrayValue; +import com.annimon.ownlang.lib.MapValue; +import com.annimon.ownlang.lib.StringValue; +import com.annimon.ownlang.lib.Types; +import com.annimon.ownlang.lib.ValueUtils; +import com.annimon.ownlang.lib.Variables; +import com.annimon.ownlang.modules.Module; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.OkHttpClient; +import okhttp3.RequestBody; + +public final class okhttp implements Module { + + private static final HttpClientValue defaultClient = new HttpClientValue(new OkHttpClient()); + + public static void initConstants() { + MapValue requestBody = new MapValue(5); + requestBody.set("bytes", args -> { + Arguments.checkOrOr(2, 4, args.length); + if (args[1].type() != Types.ARRAY) { + throw new TypeException("Array of bytes expected at second argument"); + } + final byte[] bytes = ValueUtils.toByteArray((ArrayValue) args[1]); + final int offset; + final int bytesCount; + if (args.length == 2) { + offset = 0; + bytesCount = bytes.length; + } else { + offset = args[2].asInt(); + bytesCount = args[3].asInt(); + } + return new RequestBodyValue(RequestBody.create( + MediaType.parse(args[0].asString()), + bytes, offset, bytesCount + )); + }); + requestBody.set("file", args -> { + Arguments.check(2, args.length); + return new RequestBodyValue(RequestBody.create( + MediaType.parse(args[0].asString()), + Console.fileInstance(args[1].asString()) + )); + }); + requestBody.set("string", args -> { + Arguments.check(2, args.length); + return new RequestBodyValue(RequestBody.create( + MediaType.parse(args[0].asString()), + args[1].asString() + )); + }); + Variables.define("RequestBody", requestBody); + + + MapValue multipartBody = new MapValue(10); + multipartBody.set("ALTERNATIVE", new StringValue(MultipartBody.ALTERNATIVE.toString())); + multipartBody.set("DIGEST", new StringValue(MultipartBody.DIGEST.toString())); + multipartBody.set("FORM", new StringValue(MultipartBody.FORM.toString())); + multipartBody.set("MIXED", new StringValue(MultipartBody.MIXED.toString())); + multipartBody.set("PARALLEL", new StringValue(MultipartBody.PARALLEL.toString())); + multipartBody.set("builder", args -> new MultipartBodyBuilderValue()); + Variables.define("MultipartBody", multipartBody); + + + MapValue okhttp = new MapValue(5); + okhttp.set("client", defaultClient); + okhttp.set("request", args -> new RequestBuilderValue()); + Variables.define("okhttp", okhttp); + } + + @Override + public void init() { + initConstants(); + } +}