Skip to content

Commit

Permalink
add support for components as listener to S3Download
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeloffner committed Oct 31, 2023
1 parent 1c69763 commit 9e34cda
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 55 deletions.
4 changes: 2 additions & 2 deletions build.number
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#Build Number for ANT. Do not edit!
#Tue Oct 31 17:27:17 CET 2023
build.number=4
#Tue Oct 31 20:27:20 CET 2023
build.number=5
186 changes: 133 additions & 53 deletions source/java/src/org/lucee/extension/resource/s3/function/S3Download.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.Arrays;

import org.lucee.extension.resource.s3.S3;
import org.lucee.extension.resource.s3.util.print;

import com.amazonaws.services.s3.model.S3Object;

import lucee.commons.io.res.Resource;
import lucee.commons.lang.types.RefInteger;
import lucee.loader.engine.CFMLEngine;
import lucee.loader.engine.CFMLEngineFactory;
import lucee.runtime.Component;
import lucee.runtime.PageContext;
import lucee.runtime.exp.PageException;
import lucee.runtime.type.Collection.Key;
import lucee.runtime.type.FunctionArgument;
import lucee.runtime.type.UDF;
import lucee.runtime.util.Cast;
Expand Down Expand Up @@ -54,51 +57,29 @@ public Object invoke(PageContext pc, Object[] args) throws PageException {

// validate
UDF targetUDF = null;
Component targetCFC = null;
Resource targetRes = null;
int mode = 0;
int blockSize = 0;
RefInteger mode = eng.getCreationUtil().createRefInteger(0);
RefInteger blockSize = eng.getCreationUtil().createRefInteger(0);
Key INVOKE = eng.getCastUtil().toKey("invoke");
Key BEFORE = eng.getCastUtil().toKey("before");
Key AFTER = eng.getCastUtil().toKey("after");

if (target != null) {
if (target instanceof UDF) {
targetUDF = (UDF) target;
validateInvoke(pc, targetUDF, mode, blockSize, false);

// function return type
if (!(targetUDF.getReturnType() == TYPE_ANY || targetUDF.getReturnType() == TYPE_BOOLEAN)) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3,
"target", "the function invoke of the component listener must have the return type boolean.", "");

// function invoke arguments
FunctionArgument[] udfArgs = targetUDF.getFunctionArguments();
if (udfArgs.length < 1 || udfArgs.length > 1) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"you need to define an argument for the closure/function passed in following this pattern (string line|binary{number}|string{number})", "");

FunctionArgument arg = udfArgs[0];
if (!(arg.getType() == TYPE_ANY || arg.getType() == TYPE_STRING)) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"the first argument of the closuere/function need to be defined as a string or no defintion at all", "");
print.e("name:" + arg.getName());

String name = (arg.getName().getString() + "").toLowerCase().trim();
if ("line".equals(name)) {
mode = MODE_LINE;
}
else if (name.startsWith("binary")) {
mode = MODE_BINARY;
blockSize = eng.getCastUtil().toIntValue(name.substring(6));
if (blockSize <= 0) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"invalid block size defintion with the argument [binary{Number}], blocksize need to be a positive number, so the argument name should for example look like this [binary1000] to get block size 1000",
"");
}
else if (name.startsWith("string")) {
mode = MODE_STRING;
blockSize = eng.getCastUtil().toIntValue(name.substring(6));
if (blockSize <= 0) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"invalid block size defintion with the argument [string{Number}], blocksize need to be a positive number, so the argument name should for example look like this [string1000] to get block size 1000",
"");
}
else {
throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"the first argument of the closuere/function need to be define an argument where the name does follow one of this patterns [line, binary(Number), string(Number) ]",
"");
}

}
else if (target instanceof Component) {
targetCFC = (Component) target;
Component csa = toComponentSpecificAccess(Component.ACCESS_PRIVATE, targetCFC);
boolean hasBefore = toFunction(csa.get(BEFORE), null) != null;
boolean hasAfter = toFunction(csa.get(AFTER), null) != null;
UDF invoke = toFunction(csa.get(INVOKE), null);
if (invoke == null) throw eng.getExceptionUtil().createFunctionException(pc, "DirectoryEvery", 2, "component",
"the listener component does not contain a instance function with name [invoke] that is required", null);
validateInvoke(pc, invoke, mode, blockSize, false);
}
else if ((targetRes = S3Write.toResource(pc, target, false, null)) == null) {
// can also be a charset defintion
Expand All @@ -115,16 +96,32 @@ else if ((targetRes = S3Write.toResource(pc, target, false, null)) == null) {
S3Object obj = s3.getData(bucketName, objectName);
Cast caster = eng.getCastUtil();
// stream to UDF
if (targetUDF != null) {
boolean isUDF;
if ((isUDF = (targetUDF != null)) || targetCFC != null) {

// LINE
if (MODE_LINE == mode) {
if (MODE_LINE == mode.toInt()) {
BufferedReader reader = null;
try {
if (charset == null) charset = pc.getConfig().getResourceCharset();
reader = new BufferedReader(new InputStreamReader(obj.getObjectContent(), charset));
String line;
if (!isUDF) {
targetCFC.call(pc, BEFORE, new Object[] {});
}
while ((line = reader.readLine()) != null) {
if (!caster.toBooleanValue(targetUDF.call(pc, new Object[] { line }, true))) return null;
if (isUDF) {
if (!caster.toBooleanValue(targetUDF.call(pc, new Object[] { line }, true))) return null;
}
else {
if (!caster.toBooleanValue(targetCFC.call(pc, INVOKE, new Object[] { line }))) {
targetCFC.call(pc, AFTER, new Object[] {});
return null;
}
}
}
if (!isUDF) {
targetCFC.call(pc, AFTER, new Object[] {});
}
return null;
}
Expand All @@ -133,16 +130,30 @@ else if ((targetRes = S3Write.toResource(pc, target, false, null)) == null) {
}
}
// STRING
else if (MODE_STRING == mode) {
else if (MODE_STRING == mode.toInt()) {
BufferedReader reader = null;
char[] buffer = new char[blockSize];
char[] buffer = new char[blockSize.toInt()];
try {
if (!isUDF) {
targetCFC.call(pc, BEFORE, new Object[] {});
}
if (charset == null) charset = pc.getConfig().getResourceCharset();
reader = new BufferedReader(new InputStreamReader(obj.getObjectContent(), charset));
int numCharsRead;
while ((numCharsRead = reader.read(buffer, 0, buffer.length)) != -1) {
String block = new String(buffer, 0, numCharsRead);
if (!caster.toBooleanValue(targetUDF.call(pc, new Object[] { block }, true))) return null;
if (isUDF) {
if (!caster.toBooleanValue(targetUDF.call(pc, new Object[] { block }, true))) return null;
}
else {
if (!caster.toBooleanValue(targetCFC.call(pc, INVOKE, new Object[] { block }))) {
targetCFC.call(pc, AFTER, new Object[] {});
return null;
}
}
}
if (!isUDF) {
targetCFC.call(pc, AFTER, new Object[] {});
}
return null;
}
Expand All @@ -153,20 +164,31 @@ else if (MODE_STRING == mode) {
// BINARY
else {
InputStream input = null;
byte[] buffer = new byte[blockSize];
byte[] buffer = new byte[blockSize.toInt()];
byte[] arg;
try {
if (!isUDF) {
targetCFC.call(pc, BEFORE, new Object[] {});
}
input = new BufferedInputStream(obj.getObjectContent());
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
if (bytesRead == buffer.length) {
if (!caster.toBooleanValue(targetUDF.call(pc, new Object[] { buffer }, true))) return null;

if (bytesRead == buffer.length) arg = buffer;
else arg = Arrays.copyOf(buffer, bytesRead);
if (isUDF) {
if (!caster.toBooleanValue(targetUDF.call(pc, new Object[] { arg }, true))) return null;
}
else {
// create a smaller array for the last block, which might not be a full 1000 bytes
byte[] lastBlock = Arrays.copyOf(buffer, bytesRead);
targetUDF.call(pc, new Object[] { lastBlock }, true);
if (!caster.toBooleanValue(targetCFC.call(pc, INVOKE, new Object[] { arg }))) {
targetCFC.call(pc, AFTER, new Object[] {});
return null;
}
}
}
if (!isUDF) {
targetCFC.call(pc, AFTER, new Object[] {});
}
return null;
}
finally {
Expand Down Expand Up @@ -195,4 +217,62 @@ else if (targetRes != null) {
throw eng.getCastUtil().toPageException(e);
}
}

private UDF toFunction(Object obj, UDF defaultValue) {
if (obj instanceof UDF) return (UDF) obj;
return defaultValue;
}

private void validateInvoke(PageContext pc, UDF targetUDF, RefInteger mode, RefInteger blockSize, boolean member) throws PageException {

String nameDesc = member ? "function [invoke] of the listener component" : "closure/function";

CFMLEngine eng = CFMLEngineFactory.getInstance();
// function return type
if (!(targetUDF.getReturnType() == TYPE_ANY || targetUDF.getReturnType() == TYPE_BOOLEAN))
throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target", "the " + nameDesc + " must have the return type boolean.", "");

// function invoke arguments
FunctionArgument[] udfArgs = targetUDF.getFunctionArguments();
if (udfArgs.length < 1 || udfArgs.length > 1) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"you need to define an argument for the " + nameDesc + " passed in following this pattern (string line|binary{number}|string{number})", "");

FunctionArgument arg = udfArgs[0];
if (!(arg.getType() == TYPE_ANY || arg.getType() == TYPE_STRING)) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"the first argument of the " + nameDesc + " need to be defined as a string or no defintion at all", "");

String name = (arg.getName().getString() + "").toLowerCase().trim();
if ("line".equals(name)) {
mode.setValue(MODE_LINE);
}
else if (name.startsWith("binary")) {
mode.setValue(MODE_BINARY);
blockSize.setValue(eng.getCastUtil().toIntValue(name.substring(6)));
if (blockSize.toInt() <= 0) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"invalid block size defintion with the argument [binary{Number}], blocksize need to be a positive number, so the argument name should for example look like this [binary1000] to get block size 1000",
"");
}
else if (name.startsWith("string")) {
mode.setValue(MODE_STRING);
blockSize.setValue(eng.getCastUtil().toIntValue(name.substring(6)));
if (blockSize.toInt() <= 0) throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target",
"invalid block size defintion with the argument [string{Number}], blocksize need to be a positive number, so the argument name should for example look like this [string1000] to get block size 1000",
"");
}
else {
throw eng.getExceptionUtil().createFunctionException(pc, "S3Download", 3, "target", "the first argument of the " + nameDesc
+ " need to be define an argument where the name does follow one of this patterns [line, binary(Number), string(Number) ]", "");
}
}

public static Component toComponentSpecificAccess(int access, Component component) throws PageException {
try {
Class<?> clazz = CFMLEngineFactory.getInstance().getClassUtil().loadClass("lucee.runtime.ComponentSpecificAccess");
Method m = clazz.getMethod("toComponentSpecificAccess", new Class[] { int.class, Component.class });
return (Component) m.invoke(null, new Object[] { access, component });
}
catch (Exception e) {
throw CFMLEngineFactory.getInstance().getCastUtil().toPageException(e);
}
}
}
20 changes: 20 additions & 0 deletions tests/functions/BinaryListener.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
component {

variables.listener="";


function before() {
variables.listener="before;";
}
function invoke(binary4) {
variables.listener&=len(binary4)&";";
return true;
}
function after() {
variables.listener&="after;";
}

function getData() {
return variables.listener;
}
}
20 changes: 20 additions & 0 deletions tests/functions/LineListener.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
component {

variables.lineListener="";


function before() {
variables.lineListener="before;";
}
function invoke(line) {
variables.lineListener&=line;
return true;
}
function after() {
variables.lineListener&="after;";
}

function getData() {
return variables.lineListener;
}
}
38 changes: 38 additions & 0 deletions tests/functions/S3Download.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Sorglos";
}
});

//////// UDF ///////////
it(title="download to UDF:line", body = function( currentSpec ) {
var data="";
s3Download(bucket:bucketName,object:objectName,target:function(line){
Expand Down Expand Up @@ -105,6 +106,43 @@ Sor:4;glos:4;");
assertEquals(len(data),6);
assertEquals(data, "5;5;2;");
});



//////// COMPONENT ///////////
it(title="download to component:line", body = function( currentSpec ) {
var listener=new LineListener();
s3Download(bucket:bucketName,object:objectName,target:listener,accessKeyId:cred.ACCESS_KEY_ID,secretAccessKey:cred.SECRET_KEY);
var data=listener.getData();
assertTrue(isSimpleValue(data));
assertEquals(len(data),12);
assertEquals(data, content);
});

it(title="download to component:line with charset", body = function( currentSpec ) {
var listener=new LineListener();
s3Download(bucket:bucketName,object:objectName,charset:"UTF-8",target:listener,accessKeyId:cred.ACCESS_KEY_ID,secretAccessKey:cred.SECRET_KEY);
var data=listener.getData();
assertTrue(isSimpleValue(data));
assertEquals(len(data),12);
assertEquals(data, content);
});

it(title="download to component:string with charset", body = function( currentSpec ) {
var listener=new StringListener();
s3Download(bucket:bucketName,object:objectName,charset:"UTF-8",target:listener,accessKeyId:cred.ACCESS_KEY_ID,secretAccessKey:cred.SECRET_KEY);
assertTrue(isSimpleValue(data));
assertEquals(len(data),12);
assertEquals(data, content);
});

it(title="download to component:binary", body = function( currentSpec ) {
var listener=new BinaryListener();
s3Download(bucket:bucketName,object:objectName,target:listener,accessKeyId:cred.ACCESS_KEY_ID,secretAccessKey:cred.SECRET_KEY);
assertTrue(isSimpleValue(data));
assertEquals(len(data),12);
assertEquals(data, content);
});

});
}
Expand Down
20 changes: 20 additions & 0 deletions tests/functions/StringListener.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
component {

variables.listener="";


function before() {
variables.listener="before;";
}
function invoke(string4) {
variables.listener&=string4;
return true;
}
function after() {
variables.listener&="after;";
}

function getData() {
return variables.listener;
}
}

0 comments on commit 9e34cda

Please sign in to comment.