Skip to content
Open
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,51 @@

#### Version 1.0.0 (TBD)

##### Command API

We've upgraded to CCL 4.0 which adds support for all Concourse operations as first-class commands in the language grammar. This release introduces the `Command` construct, fluent builders for creating `Command` objects that can be converted to CCL (and vice versa), and new APIs for executing commands directly as an alternative to the standard API methods.

* **Command Construct**: A `Command` is a type-safe representation of a single Concourse operation. Commands can be created programmatically via fluent builders or parsed from CCL text. Every `Command` can render itself as a CCL string via the `ccl()` method, and CCL strings can be parsed into `Command` objects via `Command.parse(String)`.
* Fluent builders use a state-machine pattern that guides valid parameter sequences:
```java
Command.to().add("name").as("Jeff").in(1)
Command.to().find(criteria).order(order)
Command.to().select("name", "age").from(record)
Command.is().holds(1)
Command.go().ping()
```
* **`exec()` and `submit()` Methods**: Two new families of methods provide direct command execution as an alternative to the standard API methods (which remain fully supported).
* `exec()` executes one or more commands and returns the result of the **last** command. This is useful for command chains where only the final outcome matters.
* `submit()` executes one or more commands and returns a **list** containing one result per command. This is useful when the result of every command in the batch is needed.
* Both methods execute commands sequentially in a single server round trip. If any command fails, execution stops and the exception is propagated immediately.
* Commands can be submitted as `Command` objects or as raw CCL text strings:
```java
// Execute a single command
Object result = concourse.exec(
Command.to().add("name").as("Jeff").in(1));

// Submit multiple commands and get all results
List<Object> results = concourse.submit(
Command.to().add("name").as("Jeff").in(1),
Command.to().select("name").from(1));

// Submit commands as CCL text
List<Object> results = concourse.submit(
"add name as Jeff in 1; select name from 1");
```
* **`prepare()` and `CommandGroup`**: The `prepare()` method returns a `CommandGroup` that mirrors the Concourse API with void-returning methods. Call methods on the group to accumulate operations, then pass the group to `submit()` to execute all recorded operations at once:
```java
CommandGroup group = concourse.prepare();
group.add("name", "Jeff", 1);
group.add("age", 30, 1);
group.select("name", 1);
List<Object> results = concourse.submit(group);
```
* **Concourse Shell Enhancements**: CaSH now recognizes CCL commands directly, alongside traditional Groovy method calls.
* Type `prepare` to enter prepare mode, where CCL commands are validated and queued with indexed output.
* Type `submit` to execute all queued commands in a single batch and display the results.
* Type `discard` to cancel prepare mode and clear all queued commands.

##### API Breaks and Deprecations
* Renamed `chronologize(...)` to `chronicle(...)`
* Renamed `review(...)` back to `audit(...)`, and standardized the API to return grouped commit history as `Map<Timestamp, List<String>>` instead of the deprecated flattened `Map<Timestamp, String>` shape.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -73,6 +74,8 @@
import com.cinchapi.concourse.config.ConcourseClientConfiguration;
import com.cinchapi.concourse.config.ConcourseServerConfiguration;
import com.cinchapi.concourse.lang.Criteria;
import com.cinchapi.concourse.lang.command.Command;
import com.cinchapi.concourse.lang.command.CommandGroup;
import com.cinchapi.concourse.lang.paginate.Page;
import com.cinchapi.concourse.lang.sort.Order;
import com.cinchapi.concourse.lang.sort.OrderComponent;
Expand Down Expand Up @@ -1291,6 +1294,22 @@ public <T> Map<T, Map<Diff, Set<Long>>> diff(String key,
start);
}

@Override
public Object exec(Command command) {
return invoke("exec", Command.class).with(command);
}

@Override
public Object exec(Command command, Command... more) {
return invoke("exec", Command.class, Command[].class).with(command,
more);
}

@Override
public Object exec(List<Command> commands) {
return invoke("exec", List.class).with(commands);
}

@Override
public void exit() {
invoke("exit").with();
Expand Down Expand Up @@ -2140,6 +2159,11 @@ public String getServerVersion() {
return invoke("getServerVersion").with();
}

@Override
public CommandGroup prepare() {
return invoke("prepare").with();
}

@Override
public Set<Long> insert(String json) {
return invoke("insert", String.class).with(json);
Expand Down Expand Up @@ -3030,6 +3054,37 @@ public void stage() {
invoke("stage").with();
}

@Override
public List<Object> submit(Command command) {
return invoke("submit", Command.class).with(command);
}

@Override
public List<Object> submit(Command command, Command... more) {
return invoke("submit", Command.class, Command[].class)
.with(command, more);
}

@Override
public List<Object> submit(List<Command> commands) {
return invoke("submit", List.class).with(commands);
}

@Override
public List<Object> submit(CommandGroup group) {
return invoke("submit", List.class).with(group.commands());
}

@Override
public Object exec(String ccl) {
return invoke("exec", String.class).with(ccl);
}

@Override
public List<Object> submit(String ccl) {
return invoke("submit", String.class).with(ccl);
}

@Override
public Timestamp time() {
return invoke("time").with();
Expand Down Expand Up @@ -3107,6 +3162,31 @@ protected Concourse copyConnection() {
return new Client(clazz, invoke("copyConnection").with(), loader);
}

/**
* Convert a {@link Command} into an equivalent object compatible with
* the remote server's classloader.
* <p>
* The conversion uses the {@link Command Command's} CCL string
* representation, which is safe to pass across classloader boundaries
* because {@link String} is loaded by the bootstrap classloader.
* </p>
*
* @param command the {@link Command} to convert
* @return a remote {@link Command} object
*/
private Object convertCommand(Command command) {
try {
String ccl = command.ccl();
Class<?> remoteCommandClass = loader
.loadClass(packageBase + "lang.command.Command");
return remoteCommandClass.getMethod("parse", String.class)
.invoke(null, ccl);
}
catch (Exception e) {
throw CheckedExceptions.wrapAsRuntimeException(e);
}
}

/**
* Return an invocation wrapper for the named {@code method} with the
* specified {@code parameterTypes}.
Expand Down Expand Up @@ -3141,6 +3221,16 @@ else if(parameterTypes[i] == Order.class) {
parameterTypes[i] = loader
.loadClass(packageBase + "lang.sort.Order");
}
else if(parameterTypes[i] == Command.class) {
parameterTypes[i] = loader.loadClass(
packageBase + "lang.command.Command");
}
else if(parameterTypes[i] == Command[].class) {
parameterTypes[i] = java.lang.reflect.Array.newInstance(
loader.loadClass(
packageBase + "lang.command.Command"),
0).getClass();
}
else {
continue;
}
Expand Down Expand Up @@ -3295,6 +3385,33 @@ else if(args[i] instanceof Page) {
.getMethod("of", int.class, int.class)
.invoke(null, offset, limit);
}
else if(args[i] instanceof Command) {
args[i] = convertCommand((Command) args[i]);
}
else if(args[i] instanceof Command[]) {
Command[] cmds = (Command[]) args[i];
Class<?> rc = loader.loadClass(
packageBase + "lang.command.Command");
Object remoteArray = java.lang.reflect.Array
.newInstance(rc, cmds.length);
for (int j = 0; j < cmds.length; j++) {
java.lang.reflect.Array.set(remoteArray, j,
convertCommand(cmds[j]));
}
args[i] = remoteArray;
}
else if(args[i] instanceof List) {
List<?> list = (List<?>) args[i];
if(!list.isEmpty()
&& list.get(0) instanceof Command) {
List<Object> converted = new ArrayList<>();
for (Object item : list) {
converted.add(
convertCommand((Command) item));
}
args[i] = converted;
}
}
else {
continue;
}
Expand Down Expand Up @@ -3330,6 +3447,13 @@ else if(object instanceof Set) {
}
object = transformed;
}
else if(object instanceof List) {
List<Object> transformed = Lists.newArrayList();
for (Object item : (List<?>) object) {
transformed.add(transformServerObject(item));
}
object = transformed;
}
else if(object instanceof Map) {
Map<Object, Object> transformed = new LinkedHashMap<>();
for (Entry<Object, Object> entry : ((Map<Object, Object>) object)
Expand Down
3 changes: 2 additions & 1 deletion concourse-driver-java/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ dependencies {
api "org.slf4j:log4j-over-slf4j:${slf4jVersion}"
api "org.slf4j:jcl-over-slf4j:${slf4jVersion}"
api 'com.google.code.gson:gson:2.5'
api 'com.cinchapi:ccl:3.2.1'
// api 'com.cinchapi:ccl:4.0.0-SNAPSHOT'
api group: 'com.cinchapi', name: 'ccl', version: '4.0.0-SNAPSHOT', changing:true

testImplementation project(':concourse-unit-test-core')
testImplementation 'com.github.marschall:memoryfilesystem:0.9.0'
Expand Down
Loading