diff --git a/src/main/java/chapter14/Application.java b/src/main/java/chapter14/Application.java new file mode 100644 index 0000000..ca77f84 --- /dev/null +++ b/src/main/java/chapter14/Application.java @@ -0,0 +1,25 @@ +package chapter14; + +import chapter14.args.Args; + +import java.text.ParseException; + +class Application { + + public static void main(String[] args) { + try { + Args arg = new Args("l,p#,d*", args); + boolean logging = arg.getBoolean('l'); + int port = arg.getInt('p'); + String directory = arg.getString('d'); + executeApplication(logging, port, directory); + } catch (ParseException e) { + System.out.printf("Parse error: %s\n", e.getMessage()); + } + } + + // stub + private static void executeApplication(boolean logging, int port, String directory) { + System.out.printf("logging: %b, port: %d, directory: %s\n", logging, port, directory); + } +} diff --git a/src/main/java/chapter14/args/Args.java b/src/main/java/chapter14/args/Args.java new file mode 100644 index 0000000..1f4429a --- /dev/null +++ b/src/main/java/chapter14/args/Args.java @@ -0,0 +1,263 @@ +package chapter14.args; + +import java.text.ParseException; +import java.util.*; + +public class Args { + private String schema; + private String[] args; + private boolean valid = true; + private Set unexpectedArguments = new TreeSet<>(); + private Map booleanArgs = new HashMap<>(); + private Map stringArgs = new HashMap<>(); + private Map intArgs = new HashMap<>(); + private Set argsFound = new HashSet<>(); + private int currentArgument; + private char errorArgumentId = '\0'; + private String errorParameter = "TILT"; + private ErrorCode errorCode = ErrorCode.OK; + + enum ErrorCode { + OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT + } + + public Args(String schema, String[] args) throws ParseException { + this.schema = schema; + this.args = args; + valid = parse(); + } + + private boolean parse() throws ParseException { + if (schema.length() == 0 && args.length == 0) + return true; + parseSchema(); + try { + parseArguments(); + } catch (ArgsException e) { + } + return valid; + } + + private boolean parseSchema() throws ParseException { + for (String element : schema.split(",")) { + if (element.length() > 0) { + String trimmedElement = element.trim(); + parseSchemaElement(trimmedElement); + } + } + return true; + } + + private void parseSchemaElement(String element) throws ParseException { + char elementId = element.charAt(0); + String elementTail = element.substring(1); + validateSchemaElementId(elementId); + if (isBooleanSchemaElement(elementTail)) + parseBooleanSchemaElement(elementId); + else if (isStringSchemaElement(elementTail)) + parseStringSchemaElement(elementId); + else if (isIntegerSchemaElement(elementTail)) + parseIntegerSchemaElement(elementId); + else { + throw new ParseException( + String.format("Argument: %c has invalid format: %s.", + elementId, elementTail), 0); + } + } + + private void validateSchemaElementId(char elementId) throws ParseException { + if (!Character.isLetter(elementId)) { + throw new ParseException( + "Bad character: " + elementId + " in Args format: " + schema, 0); + } + } + + private void parseBooleanSchemaElement(char elementId) { + booleanArgs.put(elementId, false); + } + + private void parseIntegerSchemaElement(char elementId) { + intArgs.put(elementId, 0); + } + + private void parseStringSchemaElement(char elementId) { + stringArgs.put(elementId, ""); + } + + private boolean isStringSchemaElement(String elementTail) { + return elementTail.equals("*"); + } + + private boolean isBooleanSchemaElement(String elementTail) { + return elementTail.length() == 0; + } + + private boolean isIntegerSchemaElement(String elementTail) { + return elementTail.equals("#"); + } + + private boolean parseArguments() throws ArgsException { + for (currentArgument = 0; currentArgument < args.length; currentArgument++) { + String arg = args[currentArgument]; + parseArgument(arg); + } + return true; + } + + private void parseArgument(String arg) throws ArgsException { + if (arg.startsWith("-")) + parseElements(arg); + } + + private void parseElements(String arg) throws ArgsException { + for (int i = 1; i < arg.length(); i++) { + parseElement(arg.charAt(i)); + } + } + + private void parseElement(char argChar) throws ArgsException { + if (setArgument(argChar)) + argsFound.add(argChar); + else { + unexpectedArguments.add(argChar); + errorCode = ErrorCode.UNEXPECTED_ARGUMENT; + valid = false; + } + } + + private boolean setArgument(char argChar) throws ArgsException { + if (isBooleanArg(argChar)) + setBooleanArg(argChar, true); + else if (isStringArg(argChar)) + setStringArg(argChar); + else if (isIntArg(argChar)) + setIntArg(argChar); + else + return false; + + return true; + } + + private boolean isIntArg(char argChar) { + return intArgs.containsKey(argChar); + } + + private void setIntArg(char argChar) throws ArgsException { + currentArgument++; + String parameter = null; + try { + parameter = args[currentArgument]; + intArgs.put(argChar, new Integer(parameter)); + } catch (ArrayIndexOutOfBoundsException e) { + valid = false; + errorArgumentId = argChar; + errorCode = ErrorCode.MISSING_INTEGER; + throw new ArgsException(); + } catch (NumberFormatException e) { + valid = false; + errorArgumentId = argChar; + errorParameter = parameter; + errorCode = ErrorCode.INVALID_INTEGER; + throw new ArgsException(); + } + } + + private void setStringArg(char argChar) { + currentArgument++; + try { + stringArgs.put(argChar, args[currentArgument]); + } catch (ArrayIndexOutOfBoundsException e) { + valid = false; + errorArgumentId = argChar; + errorCode = ErrorCode.MISSING_STRING; + } + } + + private boolean isStringArg(char argChar) { + return stringArgs.containsKey(argChar); + } + + private void setBooleanArg(char argChar, boolean value) { + booleanArgs.put(argChar, value); + } + + private boolean isBooleanArg(char argChar) { + return booleanArgs.containsKey(argChar); + } + + public int cardinality() { + return argsFound.size(); + } + + public String usage() { + if (schema.length() > 0) + return "-[" + schema + "]"; + else + return ""; + } + + public String errorMessage() throws Exception { + switch (errorCode) { + case OK: + throw new Exception("TILT: Should not get here."); + case UNEXPECTED_ARGUMENT: + return unexpectedArgumentMessage(); + case MISSING_STRING: + return String.format("Could not find string parameter for -%c.", + errorArgumentId); + case INVALID_INTEGER: + return String.format("Argument -%c expects an integer but was '%s'.", + errorArgumentId, errorParameter); + case MISSING_INTEGER: + return String.format("Could not find integer parameter for -%c.", + errorArgumentId); + } + return ""; + + } + + private String unexpectedArgumentMessage() { + StringBuffer message = new StringBuffer("Argument(s) -"); + for (char c : unexpectedArguments) { + message.append(c); + } + message.append(" unexpected."); + + return message.toString(); + } + + private boolean falseIfNull(Boolean b) { + return b == null ? false : b; + } + + private int zeroIfNull(Integer i) { + return i == null ? 0 : i; + } + + private String blankIfNull(String s) { + return s == null ? "" : s; + } + + public String getString(char arg) { + return blankIfNull(stringArgs.get(arg)); + } + + public int getInt(char arg) { + return zeroIfNull(intArgs.get(arg)); + } + + public boolean getBoolean(char arg) { + return falseIfNull(booleanArgs.get(arg)); + } + + public boolean has(char arg) { + return argsFound.contains(arg); + } + + public boolean isValid() { + return valid; + } + + private class ArgsException extends Exception { + } +} diff --git a/src/test/java/chapter14/ApplicationTest.java b/src/test/java/chapter14/ApplicationTest.java new file mode 100644 index 0000000..c000de9 --- /dev/null +++ b/src/test/java/chapter14/ApplicationTest.java @@ -0,0 +1,73 @@ +package chapter14; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ApplicationTest extends MainMethodTest { + + @Test + void booleanArguments() { + //given + String[] args = new String[]{"-l"}; + + //when + runMain(args); + + //then + assertThat(output()).contains("true"); + } + + @Test + void stringArguments() { + //given + String[] args = new String[]{"-d", "root"}; + + //when + runMain(args); + + //then + assertThat(output()).contains("root"); + } + + @Test + void integerArguments() { + //given + String[] args = new String[]{"-p", "42"}; + + //when + runMain(args); + + //then + assertThat(output()).contains("42"); + } + + @Test + void allArgumentsPresent() { + //given + String[] args = new String[]{"-l", "-p", "8080", "-d", "user"}; + + //when + runMain(args); + + //then + assertThat(output()).isEqualTo("logging: true, port: 8080, directory: user"); + } + + @Test + void noArguments() { + //given + String[] args = new String[0]; + + //when + runMain(args); + + //then + assertThat(output()).isEqualTo("logging: false, port: 0, directory:"); + } + + @Override + protected void runMain(String... args) { + Application.main(args); + } +} \ No newline at end of file diff --git a/src/test/java/chapter14/MainMethodTest.java b/src/test/java/chapter14/MainMethodTest.java new file mode 100644 index 0000000..e0bf861 --- /dev/null +++ b/src/test/java/chapter14/MainMethodTest.java @@ -0,0 +1,39 @@ +package chapter14; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.io.PrintStream; + +abstract class MainMethodTest { + private final PrintStream standardOut = System.out; + private OutputStream captor; + + @BeforeEach + protected final void init() { + setSystemOutToMyPrintStreamForCaptor(); + } + + private void setSystemOutToMyPrintStreamForCaptor() { + captor = new ByteArrayOutputStream(); + System.setOut(new PrintStream(captor)); + } + + @AfterEach + protected final void printOutput() { + setSystemOutToStandard(); + System.out.println(output()); + } + + private void setSystemOutToStandard() { + System.setOut(standardOut); + } + + protected final String output() { + return captor.toString().trim(); + } + + protected abstract void runMain(String... args); +} diff --git a/src/test/java/chapter14/args/ArgsTest.java b/src/test/java/chapter14/args/ArgsTest.java new file mode 100644 index 0000000..ab97b0e --- /dev/null +++ b/src/test/java/chapter14/args/ArgsTest.java @@ -0,0 +1,363 @@ +package chapter14.args; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.text.ParseException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchException; + +class ArgsTest { + + @DisplayName("schema와 arguments가 없는 경우") + @Test + void noSchemaAndArguments() throws Exception { + //given + String schema = ""; + String[] arguments = new String[0]; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isZero(); + } + + @DisplayName("schema가 없고 argument가 1개 있는 경우") + @Test + void noSchemaButWithOneArgument() throws Exception { + //given + String schema = ""; + String[] arguments = new String[]{"-x"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isFalse(); + assertThat(args.cardinality()).isZero(); + assertThat(args.errorMessage()).isEqualTo("Argument(s) -x unexpected."); + } + + @DisplayName("schema가 없고 argument가 여러 개 있는 경우") + @Test + void noSchemaButWithMultipleArguments() throws Exception { + //given + String schema = ""; + String[] arguments = new String[]{"-x", "-y"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isFalse(); + assertThat(args.cardinality()).isZero(); + assertThat(args.errorMessage()).isEqualTo("Argument(s) -xy unexpected."); + } + + @DisplayName("Schema가 있고, arugment가 없는 경우") + @Test + void multipleSchemasButNoArguments() throws Exception { + //given + String schema = "x,y#,z*"; + String[] arguments = new String[]{}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isZero(); + assertThat(args.has('x')).isFalse(); + assertThat(args.has('y')).isFalse(); + assertThat(args.has('z')).isFalse(); + assertThat(args.getBoolean('x')).isFalse(); + assertThat(args.getInt('y')).isZero(); + assertThat(args.getString('z')).isEmpty(); + } + + @DisplayName("schema가 문자가 아닌 경우") + @Test + void nonLetterSchema() { + //given + String nonLetterSchemaElementId = "*"; + String[] arguments = new String[]{}; + + //when + Exception exception = catchException(() -> new Args(nonLetterSchemaElementId, arguments)); + + //then + assertThat(exception) + .isInstanceOf(ParseException.class) + .hasMessage("Bad character: * in Args format: *"); + } + + @DisplayName("schema format이 유효하지 않은 경우") + @Test + void invalidArgumentFormat() { + //given + String invalidSchemeFormat = "f~"; + String[] arguments = new String[]{}; + + //when + Exception exception = catchException(() -> new Args(invalidSchemeFormat, arguments)); + + //then + assertThat(exception) + .isInstanceOf(ParseException.class) + .hasMessage("Argument: f has invalid format: ~."); + } + + @DisplayName("Schema format에 스페이스가 있는 경우") + @Test + void spacesInFormat() throws Exception { + //given + String schema = "x, y"; + String[] arguments = new String[]{"-xy"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isEqualTo(2); + assertThat(args.has('x')).isTrue(); + assertThat(args.has('y')).isTrue(); + } + + @DisplayName("boolean 값이 있는 경우") + @Test + void simpleBooleanPresent() throws Exception { + //given + String schema = "x"; + String[] arguments = new String[]{"-x"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isOne(); + assertThat(args.getBoolean('x')).isTrue(); + } + + @DisplayName("boolean 값이 여러 개 있는 경우") + @Test + void simpleBooleanMultiplePresent() throws Exception { + //given + String schema = "x,y,z"; + String[] arguments = new String[]{"-x", "-y"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isEqualTo(2); + assertThat(args.has('x')).isTrue(); + assertThat(args.has('y')).isTrue(); + assertThat(args.has('z')).isFalse(); + assertThat(args.getBoolean('x')).isTrue(); + assertThat(args.getBoolean('y')).isTrue(); + assertThat(args.getBoolean('z')).isFalse(); + } + + @DisplayName("boolean 값이 없는 경우") + @Test + void simpleBooleanNotPresent() throws Exception { + //given + Args args = new Args("x", new String[]{}); + + //when + boolean actual = args.getBoolean('y'); + + //then + assertThat(actual).isFalse(); + } + + @DisplayName("String 값이 있는 경우") + @Test + void simpleStringPresent() throws Exception { + //given + String schema = "x*"; + String[] arguments = new String[]{"-x", "param"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isOne(); + assertThat(args.has('x')).isTrue(); + assertThat(args.getString('x')).isEqualTo("param"); + } + + @DisplayName("String 값이 여러 개 있는 경우") + @Test + void simpleStringMultiplePresent() throws Exception { + //given + String schema = "x*,y*,z*"; + String[] arguments = new String[]{"-x", "param1", "-y", "param2"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isEqualTo(2); + assertThat(args.has('x')).isTrue(); + assertThat(args.has('y')).isTrue(); + assertThat(args.has('z')).isFalse(); + assertThat(args.getString('x')).isEqualTo("param1"); + assertThat(args.getString('y')).isEqualTo("param2"); + assertThat(args.getString('z')).isEmpty(); + } + + @DisplayName("String 값이 없는 경우") + @Test + void simpleStringNotPresent() throws Exception { + //given + Args args = new Args("x*", new String[]{}); + + //when + String actual = args.getString('y'); + + //then + assertThat(actual).isEmpty(); + } + + @DisplayName("String Argument 값이 없는 경우") + @Test + void missingStringArgument() throws Exception { + //given + String schema = "x*"; + String[] arguments = new String[]{"-x"}; // missing + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isFalse(); + assertThat(args.getString('x')).isEmpty(); + assertThat(args.errorMessage()).isEqualTo("Could not find string parameter for -x."); + } + + @DisplayName("int 값이 있는 경우") + @Test + void simpleIntPresent() throws Exception { + //given + String schema = "x#"; + String[] arguments = new String[]{"-x", "42"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isOne(); + assertThat(args.has('x')).isTrue(); + assertThat(args.getInt('x')).isEqualTo(42); + } + + @DisplayName("int 값이 여러 개 있는 경우") + @Test + void simpleIntMultiplePresent() throws Exception { + //given + String schema = "x#,y#,z#"; + String[] arguments = new String[]{"-x", "8001", "-y", "8002"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isTrue(); + assertThat(args.cardinality()).isEqualTo(2); + assertThat(args.has('x')).isTrue(); + assertThat(args.has('y')).isTrue(); + assertThat(args.has('z')).isFalse(); + assertThat(args.getInt('x')).isEqualTo(8001); + assertThat(args.getInt('y')).isEqualTo(8002); + assertThat(args.getInt('z')).isZero(); + } + + @DisplayName("int 값이 없는 경우") + @Test + void simpleIntNotPresent() throws Exception { + //given + Args args = new Args("x#", new String[]{}); + + //when + int actual = args.getInt('y'); + + //then + assertThat(actual).isZero(); + } + + @DisplayName("Integer Argument 값이 없는 경우") + @Test + void missingInteger() throws Exception { + //given + String schema = "x#"; + String[] arguments = new String[]{"-x"}; // missing + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isFalse(); + assertThat(args.getInt('x')).isZero(); + assertThat(args.errorMessage()).isEqualTo("Could not find integer parameter for -x."); + } + + @DisplayName("Integer Argument 값을 파싱할 수 없는 경우") + @Test + void invalidInteger() throws Exception { + //given + String schema = "x#"; + String[] arguments = new String[]{"-x", "Forty two"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.isValid()).isFalse(); + assertThat(args.cardinality()).isZero(); + assertThat(args.has('x')).isFalse(); + assertThat(args.getInt('x')).isZero(); + assertThat(args.errorMessage()).isEqualTo("Argument -x expects an integer but was 'Forty two'."); + } + + @DisplayName("잘못된 타입으로 호출한 경우") + @Test + void invalidType() throws Exception { + //given + String schema = "x#,y*"; + String[] arguments = new String[]{"-x", "42", "-y", "param"}; + + //when + Args args = new Args(schema, arguments); + + //then + assertThat(args.getBoolean('x')).isFalse(); // x is type int + assertThat(args.getString('x')).isEmpty(); // x is type int + assertThat(args.getInt('y')).isZero(); // y is type String + } + + @DisplayName("유효한 args에서 errorMessage를 조회할 경우 Excpetion이 발생한다") + @Test + void errorMessageThrowExceptionWhenErrorCodeIsOK() throws Exception { + //given + Args args = new Args("x", new String[]{"-x"}); + assertThat(args.isValid()).isTrue(); + + //when + Exception exception = catchException(args::errorMessage); + + //then + assertThat(exception) + .isInstanceOf(Exception.class) + .hasMessage("TILT: Should not get here."); + } +} \ No newline at end of file