Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion gorscripts/src/main/java/org/gorpipe/gor/cli/GorCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.gorpipe.gor.cli.help.HelpCommand;
import org.gorpipe.gor.cli.index.IndexCommand;
import org.gorpipe.gor.cli.info.InfoCommand;
import org.gorpipe.gor.cli.link.LinkCommand;
import org.gorpipe.gor.cli.manager.ManagerCommand;
import org.gorpipe.gor.cli.migrator.FolderMigratorCommand;
import org.gorpipe.gor.cli.query.QueryCommand;
Expand All @@ -38,7 +39,8 @@
version="version 1.0",
description = "Command line interface for gor query language and processes.",
subcommands = {QueryCommand.class, HelpCommand.class, ManagerCommand.class, IndexCommand.class,
CacheCommand.class, RenderCommand.class, InfoCommand.class, FolderMigratorCommand.class})
CacheCommand.class, RenderCommand.class, InfoCommand.class, FolderMigratorCommand.class,
LinkCommand.class})
public class GorCLI extends HelpOptions implements Runnable {
public static void main(String[] args) {
GorLogbackUtil.initLog("gor");
Expand Down
17 changes: 17 additions & 0 deletions gorscripts/src/main/java/org/gorpipe/gor/cli/link/LinkCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.gorpipe.gor.cli.link;

import org.gorpipe.gor.cli.HelpOptions;
import picocli.CommandLine;

@SuppressWarnings("squid:S106")
@CommandLine.Command(name = "link",
description = "Manage link files (create, update, rollback).",
header = "Link file management commands.",
subcommands = {LinkUpdateCommand.class, LinkRollbackCommand.class, LinkResolveCommand.class})
public class LinkCommand extends HelpOptions implements Runnable {

@Override
public void run() {
CommandLine.usage(this, System.err);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.gorpipe.gor.cli.link;

import java.io.IOException;
import java.time.Instant;

import org.gorpipe.gor.driver.linkfile.LinkFile;
import org.gorpipe.util.DateUtils;
import org.gorpipe.util.Strings;

import picocli.CommandLine;

@CommandLine.Command(name = "resolve",
description = "Resolve a link file to the entry active at the current or given time.")
public class LinkResolveCommand implements Runnable {

@CommandLine.Parameters(index = "0", paramLabel = "LINK_FILE",
description = "Path to the link file to resolve.")
private String linkFilePath;

@CommandLine.Option(names = {"-d", "--date"}, paramLabel = "DATE",
description = "ISO-8601 date/time or epoch milliseconds to resolve at (default: now).")
private String resolveDate;

@CommandLine.Option(names = {"-f", "--full-entry"},
description = "Return the full link file entry instead of only the resolved URL.")
private boolean returnFullEntry;

@Override
public void run() {
var normalizedLinkPath = LinkFile.validateAndUpdateLinkFileName(linkFilePath);
try {
var linkFile = LinkFile.load(LinkStreamSourceProvider.resolve(normalizedLinkPath, true, this));
long timestamp = resolveDate == null ? System.currentTimeMillis() : parseDate(resolveDate);
var entry = linkFile.getEntry(timestamp);
if (entry == null) {
throw new CommandLine.ParameterException(new CommandLine(this),
"No link entry found for the requested time.");
}
String output;
if (returnFullEntry) {
output = entry.format();
} else {
var resolved = linkFile.getEntryUrl(timestamp);
if (Strings.isNullOrEmpty(resolved)) {
throw new CommandLine.ParameterException(new CommandLine(this),
"No link entry found for the requested time.");
}
output = resolved;
}
System.out.println(output);
} catch (IOException e) {
throw new CommandLine.ExecutionException(new CommandLine(this),
"Failed to load link file: " + normalizedLinkPath, e);
}
}

private long parseDate(String dateValue) {
try {
Instant instant = DateUtils.parseDateISOEpoch(dateValue, true);
return instant.toEpochMilli();
} catch (Exception e) {
throw new CommandLine.ParameterException(new CommandLine(this),
"Invalid date value: " + dateValue, e);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.gorpipe.gor.cli.link;

import java.io.IOException;
import java.time.Instant;

import org.gorpipe.gor.driver.linkfile.LinkFile;
import org.gorpipe.util.DateUtils;

import picocli.CommandLine;

@CommandLine.Command(name = "rollback",
description = "Rollback the latest entry or rollback entries newer than a given date.")
public class LinkRollbackCommand implements Runnable {

@CommandLine.Parameters(index = "0", paramLabel = "LINK_FILE", description = "Path to the link file to rollback.")
private String linkFilePath;

@CommandLine.Option(names = {"-d", "--date"}, paramLabel = "DATE",
description = "ISO-8601 date/time or epoch milliseconds to rollback to (entries newer than this are removed).")
private String rollbackDate;

@Override
public void run() {
var normalizedLinkPath = LinkFile.validateAndUpdateLinkFileName(linkFilePath);
try {
var linkFile = LinkFile.load(LinkStreamSourceProvider.resolve(normalizedLinkPath, true, this));
boolean changed = rollbackDate == null ? linkFile.rollbackLatestEntry() : linkFile.rollbackToTimestamp(parseDate(rollbackDate));
if (!changed) {
throw new CommandLine.ParameterException(new CommandLine(this),
"No entries were removed. Link file may already be at the requested state.");
}
linkFile.save();
System.err.printf("Rolled back link file %s%n", normalizedLinkPath);
} catch (IOException e) {
throw new CommandLine.ExecutionException(new CommandLine(this),
"Failed to load link file: " + normalizedLinkPath, e);
}
}

private long parseDate(String dateValue) {
try {
Instant instant = DateUtils.parseDateISOEpoch(dateValue, true);
return instant.toEpochMilli();
} catch (Exception e) {
throw new CommandLine.ParameterException(new CommandLine(this),
"Invalid date value: " + dateValue, e);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.gorpipe.gor.cli.link;

import org.gorpipe.gor.driver.providers.stream.sources.StreamSource;
import org.gorpipe.gor.model.DriverBackedFileReader;
import picocli.CommandLine;

final class LinkStreamSourceProvider {

private LinkStreamSourceProvider() {
}

static StreamSource resolve(String linkPath, boolean writeable, Object commandInstance) {
var fileReader = new DriverBackedFileReader("", null);
var dataSource = fileReader.resolveUrl(linkPath, writeable);
if (dataSource == null) {
throw new CommandLine.ExecutionException(new CommandLine(commandInstance),
"Could not resolve link file path: " + linkPath);
}
if (dataSource instanceof StreamSource streamSource) {
return streamSource;
}
throw new CommandLine.ExecutionException(new CommandLine(commandInstance),
"Link path is not stream compatible: " + linkPath);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.gorpipe.gor.cli.link;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import org.gorpipe.gor.driver.linkfile.LinkFile;
import org.gorpipe.util.Strings;

import picocli.CommandLine;

@CommandLine.Command(name = "update",
description = "Append a new entry to a link file, creating the file if needed.")
public class LinkUpdateCommand implements Runnable {

@CommandLine.Parameters(index = "0", paramLabel = "LINK_FILE", description = "Path to the link file to update.")
private String linkFilePath;

@CommandLine.Parameters(index = "1", paramLabel = "LINK_VALUE", description = "Value to add to the link file (file path, URL or query).")
private String linkValue;

@CommandLine.Option(names = {"-m", "--md5"}, paramLabel = "MD5",
description = "MD5 checksum to associate with the new link entry.")
private String entryMd5;

@CommandLine.Option(names = {"-i", "--info"}, paramLabel = "INFO",
description = "Free-form info string to store with the new link entry.")
private String entryInfo;

@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
@CommandLine.Option(names = {"-h", "--header"}, paramLabel = "KEY=VALUE",
description = "Header property to upsert in the link file metadata. Repeatable.",
mapFallbackValue = "")
private final Map<String, String> headerParams = new LinkedHashMap<>();

@Override
public void run() {
var normalizedLinkPath = LinkFile.validateAndUpdateLinkFileName(linkFilePath);
try {
var linkFile = LinkFile.load(LinkStreamSourceProvider.resolve(normalizedLinkPath, true, this));
applyHeaders(linkFile);
linkFile.appendEntry(linkValue, entryMd5, entryInfo);
linkFile.save();
System.out.printf("Updated link file %s with %s%n", normalizedLinkPath, linkValue);
} catch (IOException e) {
throw new CommandLine.ExecutionException(new CommandLine(this),
"Failed to load or create link file: " + normalizedLinkPath, e);
}
}

private void applyHeaders(LinkFile linkFile) {
for (var entry : headerParams.entrySet()) {
var key = entry.getKey();
var value = entry.getValue();
if (Strings.isNullOrEmpty(key)) {
continue;
}
linkFile.getMeta().setProperty(key.trim().toUpperCase(), value != null ? value.trim() : "");
}
}
}

154 changes: 154 additions & 0 deletions gorscripts/src/test/java/org/gorpipe/gor/cli/link/LinkCommandTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.gorpipe.gor.cli.link;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.file.Path;
import java.time.Instant;

import org.gorpipe.gor.driver.linkfile.LinkFile;
import org.gorpipe.gor.driver.providers.stream.sources.file.FileSource;
import static org.junit.Assert.assertEquals;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import picocli.CommandLine;

public class LinkCommandTest {

@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Test
public void testUpdateCreatesLinkFileAndAppliesHeaders() throws Exception {
Path linkFile = temp.getRoot().toPath().resolve("update_test.gor.link");
CommandLine cmd = new CommandLine(new LinkCommand());

int exitCode = cmd.execute("update", linkFile.toString(), "data/file1.gor", "-h", "ENTRIES_COUNT_MAX=5");
assertEquals(0, exitCode);

LinkFile link = LinkFile.load(new FileSource(linkFile));
assertEquals(1, link.getEntriesCount());
assertEquals(resolve(linkFile, "data/file1.gor"), link.getLatestEntryUrl());
assertEquals(5, link.getEntriesCountMax());
}

@Test
public void testUpdateWithMd5AndInfo() throws Exception {
Path linkFile = temp.getRoot().toPath().resolve("update_md5_info.gor.link");
CommandLine cmd = new CommandLine(new LinkCommand());

int exitCode = cmd.execute("update", linkFile.toString(), "data/file1.gor",
"-m", "abc123", "-i", "first entry");
assertEquals(0, exitCode);

LinkFile link = LinkFile.load(new FileSource(linkFile));
var latest = link.getLatestEntry();
assertEquals("abc123", latest.md5());
assertEquals("first entry", latest.info());
}

@Test
public void testRollbackLatestEntry() throws Exception {
Path linkFile = temp.getRoot().toPath().resolve("rollback_latest.gor.link");
CommandLine cmd = new CommandLine(new LinkCommand());

cmd.execute("update", linkFile.toString(), "data/file1.gor");
Thread.sleep(5);
cmd.execute("update", linkFile.toString(), "data/file2.gor");

int exitCode = cmd.execute("rollback", linkFile.toString());
assertEquals(0, exitCode);

LinkFile link = LinkFile.load(new FileSource(linkFile));
assertEquals(1, link.getEntriesCount());
assertEquals(resolve(linkFile, "data/file1.gor"), link.getLatestEntryUrl());
}

@Test
public void testRollbackToDate() throws Exception {
Path linkFile = temp.getRoot().toPath().resolve("rollback_date.gor.link");
CommandLine cmd = new CommandLine(new LinkCommand());

cmd.execute("update", linkFile.toString(), "data/file1.gor");
LinkFile first = LinkFile.load(new FileSource(linkFile));
long firstTimestamp = first.getLatestEntry().timestamp();

Thread.sleep(5);
cmd.execute("update", linkFile.toString(), "data/file2.gor");

String rollbackIso = Instant.ofEpochMilli(firstTimestamp).toString();
int exitCode = cmd.execute("rollback", linkFile.toString(), "-d", rollbackIso);
assertEquals(0, exitCode);

LinkFile link = LinkFile.load(new FileSource(linkFile));
assertEquals(1, link.getEntriesCount());
assertEquals(resolve(linkFile, "data/file1.gor"), link.getLatestEntryUrl());
}

@Test
public void testResolveLatestEntry() throws Exception {
Path linkFile = temp.getRoot().toPath().resolve("resolve_latest.gor.link");
CommandLine cmd = new CommandLine(new LinkCommand());

cmd.execute("update", linkFile.toString(), "data/file1.gor");
Thread.sleep(5);
cmd.execute("update", linkFile.toString(), "data/file2.gor");

String resolved = executeAndCapture(cmd, "resolve", linkFile.toString());
assertEquals(resolve(linkFile, "data/file2.gor"), resolved);
}

@Test
public void testResolveSpecificDate() throws Exception {
Path linkFile = temp.getRoot().toPath().resolve("resolve_date.gor.link");
CommandLine cmd = new CommandLine(new LinkCommand());

cmd.execute("update", linkFile.toString(), "data/file1.gor");
long firstTimestamp = LinkFile.load(new FileSource(linkFile)).getLatestEntry().timestamp();
Thread.sleep(5);
cmd.execute("update", linkFile.toString(), "data/file2.gor");

String resolved = executeAndCapture(cmd, "resolve", linkFile.toString(),
"-d", Instant.ofEpochMilli(firstTimestamp).toString());
assertEquals(resolve(linkFile, "data/file1.gor"), resolved);
}

@Test
public void testResolveFullEntry() throws Exception {
Path linkFile = temp.getRoot().toPath().resolve("resolve_full.gor.link");
CommandLine cmd = new CommandLine(new LinkCommand());

cmd.execute("update", linkFile.toString(), "data/file1.gor");
Thread.sleep(5);
cmd.execute("update", linkFile.toString(), "data/file2.gor");

String expectedEntry = LinkFile.load(new FileSource(linkFile)).getLatestEntry().format();
String resolved = executeAndCapture(cmd, "resolve", linkFile.toString(), "-f");
assertEquals(expectedEntry, resolved);
}

private String resolve(Path linkFile, String relative) {
return linkFile.getParent().resolve(relative).toAbsolutePath().normalize().toString();
}

private String executeAndCapture(CommandLine cmd, String... args) {
PrintStream originalOut = System.out;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
System.setOut(new PrintStream(baos, true));
try {
int exitCode = cmd.execute(args);
assertEquals(0, exitCode);
} finally {
System.setOut(originalOut);
}
String output = baos.toString();
if (output.endsWith("\r\n")) {
output = output.substring(0, output.length() - 2);
} else if (output.endsWith("\n") || output.endsWith("\r")) {
output = output.substring(0, output.length() - 1);
}
return output;
}
}

Loading
Loading