Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefix harvesting CLI #127

Merged
merged 27 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3ee311b
Add resolveAllPids to PID systems.
Pfeil Apr 6, 2023
48a1b7a
Add comments to handle protocol credential values.
Pfeil Apr 6, 2023
19a2b6f
Fix most Sonarlint hints of PR and local.
Pfeil Apr 6, 2023
9792507
Implement write-file cli commands.
Pfeil Apr 6, 2023
5adc1ae
Simplify resolveAll for handle protocol.
Pfeil Apr 6, 2023
674f8e5
Handle theoretical NPE (sonarlift).
Pfeil Apr 11, 2023
dc4245c
gitignore: ignore csv files
Pfeil Apr 11, 2023
5338929
Avoid 2nd instance of ApplicationProperties class.
Pfeil Apr 11, 2023
61abcb1
Implement CLI task "bootstrap".
Pfeil Apr 11, 2023
a3ebf03
Logging: show CLI info logging by default.
Pfeil Apr 11, 2023
952e5a5
Make all source work with all tasks.
Pfeil Apr 11, 2023
db44b04
Add query and delete tests for sandboxes pid systems.
Pfeil Apr 12, 2023
9855826
Fix possible line breaks in logging.
Pfeil Apr 21, 2023
aefd643
Add cli documentation to readme.
Pfeil Apr 21, 2023
5ed8dae
Fix missing cli option on usage prompt.
Pfeil Apr 21, 2023
edb8c30
Cli parser code: make constant static members.
Pfeil Apr 21, 2023
d30ca16
Add tests for CliTaskBootstrap
Pfeil Apr 21, 2023
15bde35
Add tests for CliTaskWriteFile
Pfeil Apr 24, 2023
0245a43
Remove unnecessary spring test annotations.
Pfeil Apr 24, 2023
2a5b041
Use file names which should be windows compatible.
Pfeil Apr 25, 2023
3e6a881
Cleanup unused imports.
Pfeil Apr 25, 2023
856a88a
Fix bootstrap storing PIDs known in beforehand.
Pfeil Apr 25, 2023
b6f7c87
Add test: Bootstrap shall not change existing entries.
Pfeil Apr 25, 2023
c1ac07e
Reduce temporal precision for test.
Pfeil Apr 25, 2023
5af2864
Merge branch 'master' into add-prefix-harvesting-cli
Pfeil May 3, 2023
c09fde9
fix: queryByType must return null if PID does not exist.
Pfeil May 3, 2023
2b8f649
feat(bootstrap): fill elasticsearch index
Pfeil May 3, 2023
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
# !/setup/build/env.sh
##############

# csv files may be generated using the Typed PID Maker CLI (write-file)
*.csv

### SPRING BOOT ###
# Ignore local settings for spring boot
# If you want to provide default settings use 'application.properties.default' and rename it while building service.
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ The Typed PID Maker enables the creation, maintenance, and validation of PIDs. I
- ✅ Search for information stored within PIDs. This includes PIDs you created, updated or resolved at some point.
- ✅ Supports the [full elastic DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html) (and requires an Elasticsearch 8 instance).
- ✅ Authentication via [JWT](https://jwt.io/introduction) or [KeyCloak](https://www.keycloak.org/)
- ✅ Bootstrap with existing PIDs in your PID Prefix (see command line options).
- ✅ Extract all your PIDs to CSV files (see command line options).

### Search example

Expand Down Expand Up @@ -118,6 +120,21 @@ Furthermore, you can use this Web interface to test single API calls in order to

Details on the version being used and other build information can be found on http://localhost:8090/actuator/info.

### Command line options

- `--spring.config.location=config/application.properties` set the configuration files location to be used. Not required if the file is in the same directory as the jar file.
- `bootstrap all-pids-from-prefix` starts the service and bootstraps all PIDs. This means:
- store the PIDs as "known PIDs" in the local database (as configured)
- send one message per PID to the message broker (if configured)
- (WIP, #128) store the PID records in the search index (if configured)
- after the bootstrap, the application will continue to run
- `bootstrap known-pids` same as above, but:
- not using all PIDs from prefix, but only the ones stored in the local database ("known PIDs")
- useful to, for example, re-send PIDs via messaging to notify new services
- `write-file all-pids-from-prefix` writes all PIDs of the configured PID prefix to a CSV file (one PID per line).
- `write-file known-pids` same as above but:
- only with the PIDs stored in the local database ("known PIDs").

## License

The KIT Data Manager is licensed under the Apache License, Version 2.0.
3 changes: 2 additions & 1 deletion config/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ logging.level.edu.kit: WARN
#logging.level.org.springframework.transaction: TRACE
logging.level.org.springframework: WARN
logging.level.org.springframework.amqp: WARN
logging.level.com.zaxxer.hikari: DEBUG
#logging.level.com.zaxxer.hikari: ERROR
logging.level.edu.kit.datamanager.pit.cli: INFO

######################
### Authentication ###
Expand Down
93 changes: 91 additions & 2 deletions src/main/java/edu/kit/datamanager/pit/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalNotification;

import edu.kit.datamanager.pit.cli.CliTaskBootstrap;
import edu.kit.datamanager.pit.cli.CliTaskWriteFile;
import edu.kit.datamanager.pit.cli.ICliTask;
import edu.kit.datamanager.pit.cli.PidSource;
import edu.kit.datamanager.pit.common.InvalidConfigException;
import edu.kit.datamanager.pit.configuration.ApplicationProperties;
import edu.kit.datamanager.pit.domain.PIDRecord;
import edu.kit.datamanager.pit.domain.TypeDefinition;
Expand All @@ -37,7 +42,9 @@

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.cache.CacheConfig;
Expand All @@ -50,6 +57,7 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
Expand Down Expand Up @@ -79,6 +87,16 @@ public class Application {

private static final Logger LOG = LoggerFactory.getLogger(Application.class);

protected static final String CMD_BOOTSTRAP = "bootstrap";
protected static final String CMD_WRITE_FILE = "write-file";

protected static final String SOURCE_FROM_PREFIX = "all-pids-from-prefix";
protected static final String SOURCE_KNOWN_PIDS = "known-pids";

protected static final String ERROR_COMMUNICATION = "Communication error: {}";
protected static final String ERROR_CONFIGURATION = "Configuration error: {}";


@Bean
@Scope("prototype")
public Logger logger(InjectionPoint injectionPoint) {
Expand Down Expand Up @@ -155,7 +173,6 @@ public TypeDefinition load(String typeIdentifier) throws IOException, URISyntaxE
});
}

@Bean
@ConfigurationProperties("pit")
public ApplicationProperties applicationProperties() {
return new ApplicationProperties();
Expand All @@ -173,8 +190,80 @@ public HttpMessageConverter<PIDRecord> simplePidRecordConverter() {
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
System.out.println("Spring is running!");

final boolean cliArgsAmountValid = args != null && args.length != 0 && args.length >= 2;

if (cliArgsAmountValid) {
ICliTask task = null;
Stream<String> pidSource = null;

if (Objects.equals(args[1], SOURCE_FROM_PREFIX)) {
try {
pidSource = PidSource.fromPrefix(context);
} catch (IOException e) {
e.printStackTrace();
LOG.error(ERROR_COMMUNICATION, e.getMessage());
exitApp(context, 1);
}
} else if (Objects.equals(args[1], SOURCE_KNOWN_PIDS)) {
pidSource = PidSource.fromKnown(context);
}

if (Objects.equals(args[0], CMD_BOOTSTRAP)) {
task = new CliTaskBootstrap(context, pidSource);
} else if (Objects.equals(args[0], CMD_WRITE_FILE)) {
task = new CliTaskWriteFile(pidSource);
}

try {
if (task != null && pidSource != null) {
// ---process task---
if (task.process()) {
exitApp(context, 0);
}
} else {
printUsage(args);
exitApp(context, 1);
}
} catch (InvalidConfigException e) {
e.printStackTrace();
LOG.error(ERROR_CONFIGURATION, e.getMessage());
exitApp(context, 1);
} catch (IOException e) {
e.printStackTrace();
LOG.error(ERROR_COMMUNICATION, e.getMessage());
exitApp(context, 1);
}
}
}

private static void printUsage(String[] args) {
String firstArg = args[0].replaceAll("[\r\n]","");
String secondArg = args[1].replaceAll("[\r\n]","");
LOG.error("Got commands: {} and {}", firstArg, secondArg);
LOG.error("CLI usage incorrect. Usage:");
LOG.error("java -jar TypedPIDMaker.jar [ACTION] [SOURCE]");
LOG.error("java -jar TypedPIDMaker.jar bootstrap all-pids-from-prefix");
LOG.error("java -jar TypedPIDMaker.jar bootstrap known-pids");
LOG.error("java -jar TypedPIDMaker.jar write-file all-pids-from-prefix");
LOG.error("java -jar TypedPIDMaker.jar write-file known-pids");
}

private static void exitApp(ConfigurableApplicationContext context, int errCode) {
context.close();
try {
Thread.sleep(2 * 1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (errCode != 0) {
LOG.error("Exited with error.");
} else {
LOG.info("Success");
}
System.exit(errCode);
}

}
91 changes: 91 additions & 0 deletions src/main/java/edu/kit/datamanager/pit/cli/CliTaskBootstrap.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package edu.kit.datamanager.pit.cli;

import java.io.IOException;
import java.time.Instant;
import java.util.Optional;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ConfigurableApplicationContext;

import edu.kit.datamanager.entities.messaging.PidRecordMessage;
import edu.kit.datamanager.pit.common.InvalidConfigException;
import edu.kit.datamanager.pit.configuration.ApplicationProperties;
import edu.kit.datamanager.pit.domain.PIDRecord;
import edu.kit.datamanager.pit.elasticsearch.PidRecordElasticRepository;
import edu.kit.datamanager.pit.elasticsearch.PidRecordElasticWrapper;
import edu.kit.datamanager.pit.pidlog.KnownPid;
import edu.kit.datamanager.pit.pidlog.KnownPidsDao;
import edu.kit.datamanager.pit.pitservice.ITypingService;
import edu.kit.datamanager.service.IMessagingService;
import edu.kit.datamanager.util.AuthenticationHelper;
import edu.kit.datamanager.util.ControllerUtils;

public class CliTaskBootstrap implements ICliTask {

private static final Logger LOG = LoggerFactory.getLogger(CliTaskBootstrap.class);

protected Stream<String> pids;
protected ApplicationProperties appProps;
protected KnownPidsDao knownPids;
protected IMessagingService messagingService;
protected Optional<PidRecordElasticRepository> elastic;
protected ITypingService typingService;

public CliTaskBootstrap(ConfigurableApplicationContext context, Stream<String> pids) {
this.pids = pids;
this.appProps = context.getBean(ApplicationProperties.class);
this.knownPids = context.getBean(KnownPidsDao.class);
this.messagingService = context.getBean(IMessagingService.class);
this.elastic = this.getBeanOptional(context, PidRecordElasticRepository.class);
this.typingService = context.getBean(ITypingService.class);
}

private <T> Optional<T> getBeanOptional(ConfigurableApplicationContext context, Class<T> clazz) {
try {
return Optional.of(context.getBean(clazz));
} catch (NoSuchBeanDefinitionException e) {
return Optional.empty();
}
}

@Override
public boolean process() throws IOException, InvalidConfigException {
Instant unknownTime = Instant.ofEpochMilli(0);
pids
.map(pid -> new KnownPid(pid, unknownTime, unknownTime))
.forEach(known -> {
// store PIDs in the local database of known PIDs
LOG.info("Store PID {} in the local database of known PIDs.", known.getPid());
boolean exists = knownPids.findByPid(known.getPid()).isPresent();
if (!exists) {
knownPids.save(known);
}
// send to message broker
PidRecordMessage message = PidRecordMessage.creation(
known.getPid(),
"",
AuthenticationHelper.getPrincipal(),
ControllerUtils.getLocalHostname()
);
LOG.info("Send PID {} to message broker.", known.getPid());
messagingService.send(message);
// store in Elasticsearch
elastic.ifPresent(elastic -> {
try {
PIDRecord rec = typingService.queryAllProperties(known.getPid());
LOG.info("Store PID {} in Elasticsearch.", known.getPid());
PidRecordElasticWrapper wrapper = new PidRecordElasticWrapper(rec, typingService.getOperations());
elastic.save(wrapper);
} catch (IOException e) {
LOG.error("Failed to query PID {}.", known.getPid(), e);
}
});
});
knownPids.flush();
return false;
}

}
62 changes: 62 additions & 0 deletions src/main/java/edu/kit/datamanager/pit/cli/CliTaskWriteFile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package edu.kit.datamanager.pit.cli;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.stream.Stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CliTaskWriteFile implements ICliTask {

private static final Logger LOG = LoggerFactory.getLogger(CliTaskWriteFile.class);

protected Stream<String> pids;

public CliTaskWriteFile(Stream<String> pids) {
this.pids = pids;
}

@Override
public boolean process() throws IOException {
String filename = createFilename();

Path path = Paths.get(filename);
ensureFileExists(path);

writeToFile(path);
return true;
}

protected void writeToFile(Path path) throws IOException {
for (Iterator<String> iter = pids.iterator(); iter.hasNext(); ) {
String pid = iter.next();
LOG.info("Storing into CSV: {}", pid);
Files.writeString(path, pid + "\n", StandardOpenOption.APPEND);
}
}

protected void ensureFileExists(Path path) throws IOException {
File f = path.toFile();
f.createNewFile();
}

protected String createFilename() {
String date = ZonedDateTime
.now(ZoneId.systemDefault())
.toString()
.replace(":", ".")
.replace("[", "(")
.replace("]", ")")
.replace("/", "-")
.replace("+", "-");
return String.format("%s.%s", date, "csv");
}
}
15 changes: 15 additions & 0 deletions src/main/java/edu/kit/datamanager/pit/cli/ICliTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package edu.kit.datamanager.pit.cli;

import java.io.IOException;
import edu.kit.datamanager.pit.common.InvalidConfigException;

public interface ICliTask {
/**
* Processes this CLI task.
*
* @return true, if application should shut down after this task.
* @throws IOException on IO issues
* @throws InvalidConfigException on configuration issues
*/
public boolean process() throws IOException, InvalidConfigException;
}
Loading