BAEL 3320 JCommander (#7971)

* init jcommander

* add model layer jcommander app

* service scaffolding

* init jcommander cli layer

* wire up services and commands

* splitter impl; validator impl; tests and cleanup

* cleanup pom

* integration tests

* fix uuid validator example

* optimise uuid regex; if-else to switch

* review comments

* fix builder formatting

* change list assertion in fetch charges tests

* missing minor edit

* move to new module libraries-3

* rm unwanted files
This commit is contained in:
Priyank Srivastava
2019-11-06 03:22:34 +05:30
committed by ashleyfrieze
parent f2d9829b91
commit e16bd73565
21 changed files with 823 additions and 0 deletions
@@ -0,0 +1,37 @@
package com.baeldung.jcommander.helloworld;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
public class HelloWorldApp {
/*
* Execute:
* mvn exec:java -Dexec.mainClass=com.baeldung.jcommander.helloworld.HelloWorldApp -q \
* -Dexec.args="--name JavaWorld"
*/
public static void main(String[] args) {
HelloWorldArgs jArgs = new HelloWorldArgs();
JCommander helloCmd = JCommander
.newBuilder()
.addObject(jArgs)
.build();
helloCmd.parse(args);
System.out.println("Hello " + jArgs.getName());
}
}
class HelloWorldArgs {
@Parameter(
names = "--name",
description = "User name",
required = true
)
private String name;
public String getName() {
return name;
}
}
@@ -0,0 +1,23 @@
package com.baeldung.jcommander.usagebilling;
import com.baeldung.jcommander.usagebilling.cli.UsageBasedBilling;
public class UsageBasedBillingApp {
/*
* Entry-point: invokes the cli passing the command-line args
*
* Invoking "Submit" sub-command:
* mvn exec:java \
-Dexec.mainClass=com.baeldung.jcommander.usagebilling.UsageBasedBillingApp -q \
-Dexec.args="submit --customer cb898e7a-f2a0-46d2-9a09-531f1cee1839 --subscription subscriptionPQRMN001 --pricing-type PRE_RATED --timestamp 2019-10-03T10:58:00 --quantity 7 --price 24.56"
*
* Invoking "Fetch" sub-command:
* mvn exec:java \
-Dexec.mainClass=com.baeldung.jcommander.usagebilling.UsageBasedBillingApp -q \
-Dexec.args="fetch --customer cb898e7a-f2a0-46d2-9a09-531f1cee1839 --subscription subscriptionPQRMN001 subscriptionPQRMN002 subscriptionPQRMN003 --itemized"
*/
public static void main(String[] args) {
new UsageBasedBilling().run(args);
}
}
@@ -0,0 +1,67 @@
package com.baeldung.jcommander.usagebilling.cli;
import com.baeldung.jcommander.usagebilling.cli.splitter.ColonParameterSplitter;
import com.baeldung.jcommander.usagebilling.cli.validator.UUIDValidator;
import com.baeldung.jcommander.usagebilling.model.CurrentChargesRequest;
import com.baeldung.jcommander.usagebilling.model.CurrentChargesResponse;
import com.baeldung.jcommander.usagebilling.service.FetchCurrentChargesService;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import lombok.Getter;
import java.util.List;
import static com.baeldung.jcommander.usagebilling.cli.UsageBasedBilling.*;
import static com.baeldung.jcommander.usagebilling.service.FetchCurrentChargesService.getDefault;
@Parameters(
commandNames = { FETCH_CMD },
commandDescription = "Fetch charges for a customer in the current month, can be itemized or aggregated"
)
@Getter
class FetchCurrentChargesCommand {
FetchCurrentChargesCommand() {
}
private FetchCurrentChargesService service = getDefault();
@Parameter(names = "--help", help = true)
private boolean help;
@Parameter(
names = { "--customer", "-C" },
description = "Id of the Customer who's using the services",
validateWith = UUIDValidator.class,
order = 1,
required = true
)
private String customerId;
@Parameter(
names = { "--subscription", "-S" },
description = "Filter charges for specific subscription Ids, includes all subscriptions if no value is specified",
variableArity = true,
splitter = ColonParameterSplitter.class,
order = 2
)
private List<String> subscriptionIds;
@Parameter(
names = { "--itemized" },
description = "Whether the response should contain breakdown by subscription, only aggregate values are returned by default",
order = 3
)
private boolean itemized;
void fetch() {
CurrentChargesRequest req = CurrentChargesRequest.builder()
.customerId(customerId)
.subscriptionIds(subscriptionIds)
.itemized(itemized)
.build();
CurrentChargesResponse response = service.fetch(req);
System.out.println(response);
}
}
@@ -0,0 +1,96 @@
package com.baeldung.jcommander.usagebilling.cli;
import com.baeldung.jcommander.usagebilling.cli.converter.ISO8601TimestampConverter;
import com.baeldung.jcommander.usagebilling.cli.validator.UUIDValidator;
import com.baeldung.jcommander.usagebilling.model.UsageRequest;
import com.baeldung.jcommander.usagebilling.model.UsageRequest.PricingType;
import com.baeldung.jcommander.usagebilling.service.SubmitUsageService;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import lombok.Getter;
import java.math.BigDecimal;
import java.time.Instant;
import static com.baeldung.jcommander.usagebilling.cli.UsageBasedBilling.*;
import static com.baeldung.jcommander.usagebilling.service.SubmitUsageService.getDefault;
@Parameters(
commandNames = { SUBMIT_CMD },
commandDescription = "Submit usage for a given customer and subscription, accepts one usage item"
)
@Getter
class SubmitUsageCommand {
SubmitUsageCommand() {
}
private SubmitUsageService service = getDefault();
@Parameter(names = "--help", help = true)
private boolean help;
@Parameter(
names = { "--customer", "-C" },
description = "Id of the Customer who's using the services",
validateWith = UUIDValidator.class,
order = 1,
required = true
)
private String customerId;
@Parameter(
names = { "--subscription", "-S" },
description = "Id of the Subscription that was purchased",
order = 2,
required = true
)
private String subscriptionId;
@Parameter(
names = { "--pricing-type", "-P" },
description = "Pricing type of the usage reported",
order = 3,
required = true
)
private PricingType pricingType;
@Parameter(
names = { "--quantity" },
description = "Used quantity; reported quantity is added over the billing period",
order = 3,
required = true
)
private Integer quantity;
@Parameter(
names = { "--timestamp" },
description = "Timestamp of the usage event, must lie in the current billing period",
converter = ISO8601TimestampConverter.class,
order = 4,
required = true
)
private Instant timestamp;
@Parameter(
names = { "--price" },
description = "If PRE_RATED, unit price to be applied per unit of usage quantity reported",
order = 5
)
private BigDecimal price;
void submit() {
UsageRequest req = UsageRequest.builder()
.customerId(customerId)
.subscriptionId(subscriptionId)
.pricingType(pricingType)
.quantity(quantity)
.timestamp(timestamp)
.price(price)
.build();
String reqId = service.submit(req);
System.out.println("Generated Request Id for reference: " + reqId);
}
}
@@ -0,0 +1,80 @@
package com.baeldung.jcommander.usagebilling.cli;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.UnixStyleUsageFormatter;
public class UsageBasedBilling {
static final String SUBMIT_CMD = "submit";
static final String FETCH_CMD = "fetch";
private JCommander jCommander;
private SubmitUsageCommand submitUsageCmd;
private FetchCurrentChargesCommand fetchChargesCmd;
public UsageBasedBilling() {
this.submitUsageCmd = new SubmitUsageCommand();
this.fetchChargesCmd = new FetchCurrentChargesCommand();
jCommander = JCommander.newBuilder()
.addObject(this)
.addCommand(submitUsageCmd)
.addCommand(fetchChargesCmd)
.build();
setUsageFormatter(SUBMIT_CMD);
setUsageFormatter(FETCH_CMD);
}
public void run(String[] args) {
String parsedCmdStr;
try {
jCommander.parse(args);
parsedCmdStr = jCommander.getParsedCommand();
switch (parsedCmdStr) {
case SUBMIT_CMD:
if (submitUsageCmd.isHelp()) {
getSubCommandHandle(SUBMIT_CMD).usage();
}
System.out.println("Parsing usage request...");
submitUsageCmd.submit();
break;
case FETCH_CMD:
if (fetchChargesCmd.isHelp()) {
getSubCommandHandle(SUBMIT_CMD).usage();
}
System.out.println("Preparing fetch query...");
fetchChargesCmd.fetch();
break;
default:
System.err.println("Invalid command: " + parsedCmdStr);
}
} catch (ParameterException e) {
System.err.println(e.getLocalizedMessage());
parsedCmdStr = jCommander.getParsedCommand();
if (parsedCmdStr != null) {
getSubCommandHandle(parsedCmdStr).usage();
} else {
jCommander.usage();
}
}
}
private JCommander getSubCommandHandle(String command) {
JCommander cmd = jCommander.getCommands().get(command);
if (cmd == null) {
System.err.println("Invalid command: " + command);
}
return cmd;
}
private void setUsageFormatter(String subCommand) {
JCommander cmd = getSubCommandHandle(subCommand);
cmd.setUsageFormatter(new UnixStyleUsageFormatter(cmd));
}
}
@@ -0,0 +1,33 @@
package com.baeldung.jcommander.usagebilling.cli.converter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.converters.BaseConverter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import static java.lang.String.format;
public class ISO8601TimestampConverter extends BaseConverter<Instant> {
private static final DateTimeFormatter TS_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss");
public ISO8601TimestampConverter(String optionName) {
super(optionName);
}
@Override
public Instant convert(String value) {
try {
return LocalDateTime
.parse(value, TS_FORMATTER)
.atOffset(ZoneOffset.UTC)
.toInstant();
} catch (DateTimeParseException e) {
throw new ParameterException(getErrorString(value, format("an ISO-8601 formatted timestamp (%s)", TS_FORMATTER.toString())));
}
}
}
@@ -0,0 +1,15 @@
package com.baeldung.jcommander.usagebilling.cli.splitter;
import com.beust.jcommander.converters.IParameterSplitter;
import java.util.List;
import static java.util.Arrays.asList;
public class ColonParameterSplitter implements IParameterSplitter {
@Override
public List<String> split(String value) {
return asList(value.split(":"));
}
}
@@ -0,0 +1,26 @@
package com.baeldung.jcommander.usagebilling.cli.validator;
import com.beust.jcommander.IParameterValidator;
import com.beust.jcommander.ParameterException;
import java.util.regex.Pattern;
public class UUIDValidator implements IParameterValidator {
private static final String UUID_REGEX =
"[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}";
@Override
public void validate(String name, String value) throws ParameterException {
if (!isValidUUID(value)) {
throw new ParameterException(
"String parameter " + value + " is not a valid UUID.");
}
}
private boolean isValidUUID(String value) {
return Pattern
.compile(UUID_REGEX)
.matcher(value).matches();
}
}
@@ -0,0 +1,16 @@
package com.baeldung.jcommander.usagebilling.model;
import lombok.*;
import java.util.List;
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Builder
@Getter
public class CurrentChargesRequest {
private String customerId;
private List<String> subscriptionIds;
private boolean itemized;
}
@@ -0,0 +1,56 @@
package com.baeldung.jcommander.usagebilling.model;
import lombok.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Builder
@Getter
public class CurrentChargesResponse {
private String customerId;
private BigDecimal amountDue;
private List<LineItem> lineItems;
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb
.append("Current Month Charges: {")
.append("\n\tcustomer: ")
.append(this.customerId)
.append("\n\ttotalAmountDue: ")
.append(this.amountDue.setScale(2, RoundingMode.HALF_UP))
.append("\n\tlineItems: [");
for (LineItem li : this.lineItems) {
sb
.append("\n\t\t{")
.append("\n\t\t\tsubscription: ")
.append(li.subscriptionId)
.append("\n\t\t\tamount: ")
.append(li.amount.setScale(2, RoundingMode.HALF_UP))
.append("\n\t\t\tquantity: ")
.append(li.quantity)
.append("\n\t\t},");
}
sb.append("\n\t]\n}\n");
return sb.toString();
}
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Builder
@Getter
public static class LineItem {
private String subscriptionId;
private BigDecimal amount;
private Integer quantity;
}
}
@@ -0,0 +1,50 @@
package com.baeldung.jcommander.usagebilling.model;
import lombok.*;
import java.math.BigDecimal;
import java.time.Instant;
@NoArgsConstructor(access = AccessLevel.PACKAGE)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Builder
@Getter
public class UsageRequest {
private String customerId;
private String subscriptionId;
private PricingType pricingType;
private Integer quantity;
private BigDecimal price;
private Instant timestamp;
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb
.append("\nUsage: {")
.append("\n\tcustomer: ")
.append(this.customerId)
.append("\n\tsubscription: ")
.append(this.subscriptionId)
.append("\n\tquantity: ")
.append(this.quantity)
.append("\n\ttimestamp: ")
.append(this.timestamp)
.append("\n\tpricingType: ")
.append(this.pricingType);
if (PricingType.PRE_RATED == this.pricingType) {
sb
.append("\n\tpreRatedAt: ")
.append(this.price);
}
sb.append("\n}\n");
return sb.toString();
}
public enum PricingType {
PRE_RATED, UNRATED
}
}
@@ -0,0 +1,68 @@
package com.baeldung.jcommander.usagebilling.service;
import com.baeldung.jcommander.usagebilling.model.CurrentChargesRequest;
import com.baeldung.jcommander.usagebilling.model.CurrentChargesResponse;
import com.baeldung.jcommander.usagebilling.model.CurrentChargesResponse.LineItem;
import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Arrays.fill;
import static java.util.Collections.emptyList;
import static java.util.concurrent.ThreadLocalRandom.current;
import static java.util.stream.Collectors.toList;
class DefaultFetchCurrentChargesService implements FetchCurrentChargesService {
@Override
public CurrentChargesResponse fetch(CurrentChargesRequest request) {
List<String> subscriptions = request.getSubscriptionIds();
if (subscriptions == null || subscriptions.isEmpty()) {
System.out.println("Fetching ALL charges for customer: " + request.getCustomerId());
subscriptions = mockSubscriptions();
} else {
System.out.println(format("Fetching charges for customer: %s and subscriptions: %s", request.getCustomerId(), subscriptions));
}
CurrentChargesResponse charges = mockCharges(request.getCustomerId(), subscriptions, request.isItemized());
System.out.println("Fetched charges...");
return charges;
}
private CurrentChargesResponse mockCharges(String customerId, List<String> subscriptions, boolean itemized) {
List<LineItem> lineItems = mockLineItems(subscriptions);
BigDecimal amountDue = lineItems
.stream()
.map(li -> li.getAmount())
.reduce(new BigDecimal("0"), BigDecimal::add);
return CurrentChargesResponse
.builder()
.customerId(customerId)
.lineItems(itemized ? lineItems : emptyList())
.amountDue(amountDue)
.build();
}
private List<LineItem> mockLineItems(List<String> subscriptions) {
return subscriptions
.stream()
.map(subscription -> LineItem.builder()
.subscriptionId(subscription)
.quantity(current().nextInt(20))
.amount(new BigDecimal(current().nextDouble(1_000)))
.build())
.collect(toList());
}
private List<String> mockSubscriptions() {
String[] subscriptions = new String[5];
fill(subscriptions, UUID.randomUUID().toString());
return asList(subscriptions);
}
}
@@ -0,0 +1,16 @@
package com.baeldung.jcommander.usagebilling.service;
import com.baeldung.jcommander.usagebilling.model.UsageRequest;
import java.util.UUID;
class DefaultSubmitUsageService implements SubmitUsageService {
@Override
public String submit(UsageRequest request) {
System.out.println("Submitting usage..." + request);
System.out.println("Submitted usage successfully...");
return UUID.randomUUID().toString();
}
}
@@ -0,0 +1,13 @@
package com.baeldung.jcommander.usagebilling.service;
import com.baeldung.jcommander.usagebilling.model.CurrentChargesRequest;
import com.baeldung.jcommander.usagebilling.model.CurrentChargesResponse;
public interface FetchCurrentChargesService {
static FetchCurrentChargesService getDefault() {
return new DefaultFetchCurrentChargesService();
}
CurrentChargesResponse fetch(CurrentChargesRequest request);
}
@@ -0,0 +1,12 @@
package com.baeldung.jcommander.usagebilling.service;
import com.baeldung.jcommander.usagebilling.model.UsageRequest;
public interface SubmitUsageService {
static SubmitUsageService getDefault() {
return new DefaultSubmitUsageService();
}
String submit(UsageRequest request);
}