b/160263407 Create command line client and arguments parser

Change-Id: I775f340cd69e4f464946894d8c3f32f6ee2a561b
diff --git a/build.gradle b/build.gradle
index 5b3649f..76a7207 100644
--- a/build.gradle
+++ b/build.gradle
@@ -21,12 +21,16 @@
 
     // Logging
     "flogger": "0.5.1", // 22 June 2020
+    "floggerBackend": "0.5.1", // 25 June 2020
     "logback": "1.2.3", // 16 January 2020
     "slf4j": "1.7.30", // 16 January 2020
 
     // Client Libraries
     "httpClient": "4.5.12", // 22 June 2020
 
+    // Helper Libraries
+    "apacheCommonsValidator": "1.4.0", // 25 June 2020
+
     // Build Utilities
     "errorprone":"2.4.0", // 22 June 2020
     "errorproneJavacVersion":"9+181-r4173-1", // 22 June 2020
@@ -43,7 +47,8 @@
 
     // OpenAPI Libraries
     "openapiParser": "1.0.1", // 23 June 2020
-    "openapiCore": "1.0.1" // 23 June 2020
+    "openapiCore": "1.0.1", // 23 June 2020
+    "openapiSchemaValidator": "1.0.2" // 7 July 2020
 ]
 
 allprojects {
@@ -76,8 +81,10 @@
         errorprone "com.google.errorprone:error_prone_core:${libVersions.errorprone}"
 
         implementation "com.google.flogger:flogger:${libVersions.flogger}"
+        implementation "com.google.flogger:flogger-system-backend:${libVersions.floggerBackend}"
         implementation "com.google.guava:guava:${libVersions.guava}"
         implementation "com.google.inject:guice:${libVersions.guice}"
+        implementation "commons-validator:commons-validator:${libVersions.apacheCommonsValidator}"
 
         //testCompile project(':oas-test')
 
@@ -103,8 +110,7 @@
 
     spotbugs {
         toolVersion = libVersions.spotbugs
-        // TODO(ttourani): Create spotbugs-exclude.xml
-        //excludeFilter = file("$rootDir/spotbugs-exclude.xml")
+        excludeFilter = file("$rootDir/spotbugs-exclude.xml")
         effort = "max"
         reportLevel = "low"
     }
diff --git a/oas-cli/build.gradle b/oas-cli/build.gradle
index f2a89ff..f00b209 100644
--- a/oas-cli/build.gradle
+++ b/oas-cli/build.gradle
@@ -1,5 +1,16 @@
+plugins {
+    id 'application'
+}
+
 dependencies {
     implementation 'com.beust:jcommander:1.78'
 
     testImplementation project(':oas-test')
-}
\ No newline at end of file
+}
+
+jar {
+    manifest {
+        attributes('Main-Class': 'com.apigee.security.oas.CommandLineClient')
+    }
+}
+mainClassName = "com.apigee.security.oas.CommandLineClient"
\ No newline at end of file
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineArgs.java b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineArgs.java
new file mode 100644
index 0000000..20f9747
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineArgs.java
@@ -0,0 +1,32 @@
+package com.apigee.security.oas;
+
+import com.apigee.security.oas.converters.FileConverter;
+import com.apigee.security.oas.validators.FileValidator;
+import com.beust.jcommander.Parameter;
+import java.io.File;
+
+/** JCommander Command Line arguments. */
+public class CommandLineArgs {
+
+  @Parameter(
+      names = {"--file", "-F"},
+      description = "File location of the OpenAPI Specification Document",
+      required = true,
+      validateWith = {FileValidator.class},
+      converter = FileConverter.class)
+  private File oasFile;
+
+  @Parameter(
+      names = {"--help", "-h"},
+      description = "List of all possible options",
+      help = true)
+  private boolean help = false;
+
+  public boolean isHelp() {
+    return help;
+  }
+
+  public File getOasFile() {
+    return oasFile;
+  }
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineBaseParser.java b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineBaseParser.java
new file mode 100644
index 0000000..06208e7
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineBaseParser.java
@@ -0,0 +1,40 @@
+package com.apigee.security.oas;
+
+import com.beust.jcommander.JCommander;
+import java.io.File;
+import javax.inject.Inject;
+
+/**
+ * Parses and validates command line arguments.
+ *
+ * <p>{@code oasFile} should be existent and accessible.
+ */
+final class CommandLineBaseParser implements CommandLineParser {
+
+  private final CommandLineArgs commandLineArgs;
+
+  @Inject
+  CommandLineBaseParser(CommandLineArgs commandLineArgs) {
+    this.commandLineArgs = commandLineArgs;
+  }
+
+  /**
+   * Parses {@code args}.
+   *
+   * <p>Provides command line usage if help parameter is true.
+   */
+  @Override
+  public void parseArguments(String[] args) {
+    JCommander jCommander = JCommander.newBuilder().addObject(commandLineArgs).build();
+    jCommander.parse(args);
+
+    if (commandLineArgs.isHelp()) {
+      jCommander.usage();
+    }
+  }
+
+  @Override
+  public File getOasFile() {
+    return commandLineArgs.getOasFile();
+  }
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineBaseRunner.java b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineBaseRunner.java
new file mode 100644
index 0000000..168084e
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineBaseRunner.java
@@ -0,0 +1,44 @@
+package com.apigee.security.oas;
+
+import com.beust.jcommander.ParameterException;
+import com.google.common.flogger.FluentLogger;
+import com.google.inject.Provider;
+import java.io.PrintWriter;
+import javax.inject.Inject;
+
+/** Class that handles the execution. */
+final class CommandLineBaseRunner implements CommandLineRunner {
+
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private final Provider<PrintWriter> printWriterProvider;
+  private final CommandLineParser commandLineParser;
+
+  @Inject
+  CommandLineBaseRunner(
+      Provider<PrintWriter> printWriterProvider, CommandLineParser commandLineParser) {
+    this.printWriterProvider = printWriterProvider;
+    this.commandLineParser = commandLineParser;
+  }
+
+  /**
+   * Calls different methods for parsing & validity of arguments, OpenAPI Specification Document
+   * (v3), and its security features.
+   *
+   * <p>Takes a {@code File} to a OpenAPI Specification (v3) document and outputs its Security
+   * Validity.
+   *
+   * @param args Arguments that are passed through command line interface.
+   */
+  @Override
+  public void run(String[] args) {
+    PrintWriter printWriter = printWriterProvider.get();
+    try {
+      commandLineParser.parseArguments(args);
+    } catch (ParameterException e) {
+      logger.atSevere().withCause(e).log("Unable to parse arguments");
+      printWriter.println(e.getLocalizedMessage());
+    } finally {
+      printWriter.close();
+    }
+  }
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineClient.java b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineClient.java
new file mode 100644
index 0000000..0b5330d
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineClient.java
@@ -0,0 +1,22 @@
+package com.apigee.security.oas;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+/**
+ * The main command line interface serves as the entry point that accepts arguments and performs
+ * argument validation, OpenAPI security validation, and rules enforcement.
+ */
+public final class CommandLineClient {
+
+  private CommandLineClient() {}
+
+  /**
+   * Creates an instance of {@link CommandLineRunner} and calls the {@link
+   * CommandLineRunner#run(String[])} method.
+   */
+  public static void main(String[] args) {
+    Injector injector = Guice.createInjector(new CommandLineModule());
+    injector.getInstance(CommandLineRunner.class).run(args);
+  }
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineInnerModule.java b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineInnerModule.java
new file mode 100644
index 0000000..db4e36a
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineInnerModule.java
@@ -0,0 +1,21 @@
+package com.apigee.security.oas;
+
+import com.apigee.security.oas.providers.PrintWriterProvider;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import java.io.PrintWriter;
+
+/** Module with instructions for instantiating instances of {@link CommandLineParser}. */
+public final class CommandLineInnerModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(CommandLineParser.class).to(CommandLineBaseParser.class);
+    bind(CommandLineRunner.class).to(CommandLineBaseRunner.class);
+    bind(PrintWriter.class).toProvider(PrintWriterProvider.class);
+  }
+
+  @Provides
+  public CommandLineArgs provideCommandLineArgs() {
+    return new CommandLineArgs();
+  }
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineModule.java b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineModule.java
new file mode 100644
index 0000000..8ec27a0
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineModule.java
@@ -0,0 +1,13 @@
+package com.apigee.security.oas;
+
+import com.google.inject.AbstractModule;
+
+/** Top level module that imports other Guice modules relied upon. */
+public final class CommandLineModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    install(new CommandLineInnerModule());
+    binder().requireExplicitBindings();
+  }
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineParser.java b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineParser.java
index 2b2bd6c..5b0b46d 100644
--- a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineParser.java
+++ b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineParser.java
@@ -1,3 +1,13 @@
 package com.apigee.security.oas;
 
-public class CommandLineParser {}
+import java.io.File;
+
+/** Parses and validates {@code args} while providing helper functions. */
+interface CommandLineParser {
+
+  /** Parses and validates {@code args}. */
+  void parseArguments(String[] args);
+
+  /** Returns {@code File} parsed through {@link #parseArguments(String[])}. */
+  File getOasFile();
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/CommandLineRunner.java b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineRunner.java
new file mode 100644
index 0000000..c1452cf
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/CommandLineRunner.java
@@ -0,0 +1,8 @@
+package com.apigee.security.oas;
+
+/** Performs end to end application logic. */
+interface CommandLineRunner {
+
+  /** Executes end to end application logic. */
+  void run(String[] args);
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/converters/FileConverter.java b/oas-cli/src/main/java/com/apigee/security/oas/converters/FileConverter.java
new file mode 100644
index 0000000..2cc65f5
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/converters/FileConverter.java
@@ -0,0 +1,13 @@
+package com.apigee.security.oas.converters;
+
+import com.beust.jcommander.IStringConverter;
+import java.io.File;
+
+/** JCommander converter class from {@link String} to {@link File}. */
+public final class FileConverter implements IStringConverter<File> {
+
+  @Override
+  public File convert(String value) {
+    return new File(value);
+  }
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/providers/PrintWriterProvider.java b/oas-cli/src/main/java/com/apigee/security/oas/providers/PrintWriterProvider.java
new file mode 100644
index 0000000..0e6cb85
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/providers/PrintWriterProvider.java
@@ -0,0 +1,17 @@
+package com.apigee.security.oas.providers;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.inject.Provider;
+import java.io.BufferedWriter;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+
+/** Provides {@link PrintWriter} with {@code System.out} output stream. */
+public class PrintWriterProvider implements Provider<PrintWriter> {
+
+  @Override
+  public PrintWriter get() {
+    return new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out, UTF_8)));
+  }
+}
diff --git a/oas-cli/src/main/java/com/apigee/security/oas/validators/FileValidator.java b/oas-cli/src/main/java/com/apigee/security/oas/validators/FileValidator.java
new file mode 100644
index 0000000..07edfde
--- /dev/null
+++ b/oas-cli/src/main/java/com/apigee/security/oas/validators/FileValidator.java
@@ -0,0 +1,19 @@
+package com.apigee.security.oas.validators;
+
+import com.beust.jcommander.IParameterValidator;
+import com.beust.jcommander.ParameterException;
+import java.io.File;
+import java.io.FileNotFoundException;
+
+/** Validates whether File exists. */
+public final class FileValidator implements IParameterValidator {
+
+  @Override
+  public void validate(String name, String value) {
+    File file = new File(value);
+    if (!file.exists()) {
+      throw new ParameterException(
+          "The file is either not valid or not accessible", new FileNotFoundException());
+    }
+  }
+}
diff --git a/oas-cli/src/test/java/com/apigee/security/oas/CommandLineBaseParserTest.java b/oas-cli/src/test/java/com/apigee/security/oas/CommandLineBaseParserTest.java
new file mode 100644
index 0000000..af4d0b0
--- /dev/null
+++ b/oas-cli/src/test/java/com/apigee/security/oas/CommandLineBaseParserTest.java
@@ -0,0 +1,79 @@
+package com.apigee.security.oas;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+import com.beust.jcommander.ParameterException;
+import com.google.common.io.Resources;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import java.io.FileNotFoundException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CommandLineBaseParserTest {
+  private static final String VALID_FILE_NAME = "validOpenApi3DemoSpec.yaml";
+  private static final URL VALID_FILE_URL = Resources.getResource(VALID_FILE_NAME);
+  private static final String INVALID_FILE_URI_PARAM = "demoOpenSpec.yaml";
+
+  private String validFileUriPath;
+
+  private CommandLineParser commandLineParser;
+
+  /** Creates a CommandLineParser Object and does other necessary initializations. */
+  @Before
+  public void setup() throws URISyntaxException {
+    validFileUriPath = VALID_FILE_URL.toURI().getPath();
+
+    Injector injector = Guice.createInjector(new CommandLineModule());
+    commandLineParser = injector.getInstance(CommandLineParser.class);
+  }
+
+  @Test
+  public void parseArguments_noParameter_throwsParameterExceptionWithMessage() {
+    String[] testArgs = new String[] {};
+    assertThatThrownBy(() -> commandLineParser.parseArguments(testArgs))
+        .isInstanceOf(ParameterException.class)
+        .hasMessageContainingAll("option", "required");
+  }
+
+  @Test
+  public void parseArguments_helpParameterPassed_doesNotThrowException() {
+    String[] testArgs = new String[] {"--help"};
+
+    assertThat(catchThrowable(() -> commandLineParser.parseArguments(testArgs)))
+        .doesNotThrowAnyException();
+  }
+
+  @Test
+  public void parseArguments_validFilePath_shouldNotThrowException() {
+    String[] testArgs = new String[] {"--file", validFileUriPath};
+
+    assertThat(catchThrowable(() -> commandLineParser.parseArguments(testArgs)))
+        .doesNotThrowAnyException();
+  }
+
+  @Test
+  public void parseArguments_invalidFilePath_throwsParameterExceptionWithCauseAndMessage() {
+    String[] testArgs = new String[] {"--file", INVALID_FILE_URI_PARAM};
+    assertThatThrownBy(() -> commandLineParser.parseArguments(testArgs))
+        .isInstanceOf(ParameterException.class)
+        .hasRootCauseInstanceOf(FileNotFoundException.class)
+        .hasMessageContaining("not valid");
+  }
+
+  @Test
+  public void parseArguments_emptyFilePath_throwsParameterExceptionWithMessage() {
+    String[] testArgs = new String[] {"--file"};
+
+    assertThatThrownBy(() -> commandLineParser.parseArguments(testArgs))
+        .isInstanceOf(ParameterException.class)
+        .hasMessageContainingAll("Expected", "value");
+  }
+}
diff --git a/oas-cli/src/test/java/com/apigee/security/oas/CommandLineBaseRunnerTest.java b/oas-cli/src/test/java/com/apigee/security/oas/CommandLineBaseRunnerTest.java
new file mode 100644
index 0000000..2fc0284
--- /dev/null
+++ b/oas-cli/src/test/java/com/apigee/security/oas/CommandLineBaseRunnerTest.java
@@ -0,0 +1,56 @@
+package com.apigee.security.oas;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.beust.jcommander.ParameterException;
+import com.google.inject.Provider;
+import java.io.PrintWriter;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(JUnit4.class)
+public class CommandLineBaseRunnerTest {
+  @Rule public MockitoRule rule = MockitoJUnit.rule();
+
+  @Mock private Provider<PrintWriter> printWriterProvider;
+  @Mock private CommandLineParser commandLineParser;
+  @Mock private PrintWriter printWriter;
+  @InjectMocks private CommandLineBaseRunner commandLineRunner;
+
+  @Before
+  public void setup() {
+    when(printWriterProvider.get()).thenReturn(printWriter);
+  }
+
+  @Test
+  public void run_callsParseArguments() {
+    commandLineRunner.run(new String[] {""});
+
+    verify(commandLineParser, atLeastOnce()).parseArguments(any(String[].class));
+  }
+
+  @Test
+  public void run_onException_printsExceptionMessageAndClosesPrintWriter() {
+    String exceptionMessage = "No Parameters received";
+    doThrow(new ParameterException(exceptionMessage))
+        .when(commandLineParser)
+        .parseArguments(any(String[].class));
+
+    commandLineRunner.run(new String[] {""});
+
+    verify(printWriter, atLeastOnce()).println(contains(exceptionMessage));
+    verify(printWriter, atLeastOnce()).close();
+  }
+}
diff --git a/oas-cli/src/test/java/com/apigee/security/oas/CommandLineModuleTest.java b/oas-cli/src/test/java/com/apigee/security/oas/CommandLineModuleTest.java
new file mode 100644
index 0000000..d5cf534
--- /dev/null
+++ b/oas-cli/src/test/java/com/apigee/security/oas/CommandLineModuleTest.java
@@ -0,0 +1,41 @@
+package com.apigee.security.oas;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import java.io.PrintWriter;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class CommandLineModuleTest {
+
+  private Injector injector;
+
+  @Before
+  public void setup() {
+    injector = Guice.createInjector(new CommandLineModule());
+  }
+
+  @Test
+  public void createCommandLineParser_shouldNotThrowException() {
+    assertThat(catchThrowable(() -> injector.getInstance(CommandLineParser.class)))
+        .doesNotThrowAnyException();
+  }
+
+  @Test
+  public void createCommandLineArgs_shouldNotThrowException() {
+    assertThat(catchThrowable(() -> injector.getInstance(CommandLineArgs.class)))
+        .doesNotThrowAnyException();
+  }
+
+  @Test
+  public void createPrintWriter_shouldNotThrowException() {
+    assertThat(catchThrowable(() -> injector.getInstance(PrintWriter.class).close()))
+        .doesNotThrowAnyException();
+  }
+}
diff --git a/oas-cli/src/test/java/com/apigee/security/oas/CommandLineParserTest.java b/oas-cli/src/test/java/com/apigee/security/oas/CommandLineParserTest.java
deleted file mode 100644
index 7727d03..0000000
--- a/oas-cli/src/test/java/com/apigee/security/oas/CommandLineParserTest.java
+++ /dev/null
@@ -1,3 +0,0 @@
-package com.apigee.security.oas;
-
-public class CommandLineParserTest {}
diff --git a/oas-cli/src/test/java/com/apigee/security/oas/FileConverterTest.java b/oas-cli/src/test/java/com/apigee/security/oas/FileConverterTest.java
new file mode 100644
index 0000000..d2f789e
--- /dev/null
+++ b/oas-cli/src/test/java/com/apigee/security/oas/FileConverterTest.java
@@ -0,0 +1,34 @@
+package com.apigee.security.oas;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.apigee.security.oas.converters.FileConverter;
+import com.google.common.io.Resources;
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FileConverterTest {
+
+  private static final String VALID_FILE_NAME = "validOpenApi3DemoSpec.yaml";
+  private static final URL VALID_FILE_URL = Resources.getResource(VALID_FILE_NAME);
+  private String validFileUriPath;
+
+  @Before
+  public void setup() throws URISyntaxException {
+    validFileUriPath = VALID_FILE_URL.toURI().getPath();
+  }
+
+  @Test
+  public void fileConverter_validFilePath_returnsExactFile() {
+    FileConverter fileConverter = new FileConverter();
+    File actualFile = new File(validFileUriPath);
+
+    assertThat(fileConverter.convert(validFileUriPath)).isEqualTo(actualFile);
+  }
+}
diff --git a/oas-cli/src/test/java/com/apigee/security/oas/FileValidatorTest.java b/oas-cli/src/test/java/com/apigee/security/oas/FileValidatorTest.java
new file mode 100644
index 0000000..2416039
--- /dev/null
+++ b/oas-cli/src/test/java/com/apigee/security/oas/FileValidatorTest.java
@@ -0,0 +1,41 @@
+package com.apigee.security.oas;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+import com.apigee.security.oas.validators.FileValidator;
+import com.beust.jcommander.ParameterException;
+import com.google.common.io.Resources;
+import java.net.URISyntaxException;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FileValidatorTest {
+  private static final String VALID_FILE_NAME = "validOpenApi3DemoSpec.yaml";
+
+  private String validFileUriParam;
+  private FileValidator fileValidator;
+
+  @Before
+  public void setup() throws URISyntaxException {
+    validFileUriParam = Resources.getResource(VALID_FILE_NAME).toURI().getPath();
+    fileValidator = new FileValidator();
+  }
+
+  @Test
+  public void validate_nonExistentFile_throwsParameterExceptionWithMessage() {
+    assertThatThrownBy(() -> fileValidator.validate("file", "nonExistentFile.yaml"))
+        .isInstanceOf(ParameterException.class)
+        .hasMessageContainingAll("file", "not valid");
+  }
+
+  @Test
+  public void validate_existentFile_doesNotThrowException() {
+    assertThat(catchThrowable(() -> fileValidator.validate("file", validFileUriParam)))
+        .doesNotThrowAnyException();
+  }
+}
diff --git a/oas-cli/src/test/java/com/apigee/security/oas/PrintWriterProviderTest.java b/oas-cli/src/test/java/com/apigee/security/oas/PrintWriterProviderTest.java
new file mode 100644
index 0000000..5fcc42b
--- /dev/null
+++ b/oas-cli/src/test/java/com/apigee/security/oas/PrintWriterProviderTest.java
@@ -0,0 +1,20 @@
+package com.apigee.security.oas;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.apigee.security.oas.providers.PrintWriterProvider;
+import java.io.PrintWriter;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class PrintWriterProviderTest {
+
+  @Test
+  public void get_returnsPrintWriter() {
+    PrintWriterProvider printWriterProvider = new PrintWriterProvider();
+
+    assertThat(printWriterProvider.get()).isInstanceOf(PrintWriter.class);
+  }
+}
diff --git a/oas-cli/src/test/resources/validOpenApi3DemoSpec.yaml b/oas-cli/src/test/resources/validOpenApi3DemoSpec.yaml
new file mode 100644
index 0000000..f87c231
--- /dev/null
+++ b/oas-cli/src/test/resources/validOpenApi3DemoSpec.yaml
@@ -0,0 +1,820 @@
+openapi: 3.0.0
+info:
+  title: OpenAPI Petstore
+  description: This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. For OAuth2 flow, you may use `user` as both username and password when asked to login.
+  license:
+    name: Apache-2.0
+    url: http://www.apache.org/licenses/LICENSE-2.0.html
+  version: 1.0.0
+externalDocs:
+  description: Find out more about OpenAPI generator
+  url: https://openapi-generator.tech
+tags:
+- name: pet
+  description: Everything about your Pets
+- name: store
+  description: Access to Petstore orders
+- name: user
+  description: Operations about user
+paths:
+  /pet:
+    put:
+      tags:
+      - pet
+      summary: Update an existing pet
+      operationId: updatePet
+      requestBody:
+        $ref: '#/components/requestBodies/Pet'
+      responses:
+        400:
+          description: Invalid ID supplied
+        404:
+          description: Pet not found
+        405:
+          description: Validation exception
+      security:
+      - petstore_auth:
+        - write:pets
+        - read:pets
+      x-accepts: application/json
+      x-tags:
+      - tag: pet
+      x-contentType: application/json
+    post:
+      tags:
+      - pet
+      summary: Add a new pet to the store
+      operationId: addPet
+      requestBody:
+        $ref: '#/components/requestBodies/Pet'
+      responses:
+        405:
+          description: Invalid input
+      security:
+      - petstore_auth:
+        - write:pets
+        - read:pets
+      x-accepts: application/json
+      x-tags:
+      - tag: pet
+      x-contentType: application/json
+  /pet/findByStatus:
+    get:
+      tags:
+      - pet
+      summary: Finds Pets by status
+      description: Multiple status values can be provided with comma separated strings
+      operationId: findPetsByStatus
+      parameters:
+      - name: status
+        in: query
+        description: Status values that need to be considered for filter
+        required: true
+        style: form
+        explode: false
+        schema:
+          type: array
+          items:
+            type: string
+            default: available
+            enum:
+            - available
+            - pending
+            - sold
+      responses:
+        200:
+          description: successful operation
+          content:
+            application/xml:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Pet'
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Pet'
+        400:
+          description: Invalid status value
+      security:
+      - petstore_auth:
+        - write:pets
+        - read:pets
+      x-accepts: application/json
+      x-tags:
+      - tag: pet
+  /pet/findByTags:
+    get:
+      tags:
+      - pet
+      summary: Finds Pets by tags
+      description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+      operationId: findPetsByTags
+      parameters:
+      - name: tags
+        in: query
+        description: Tags to filter by
+        required: true
+        style: form
+        explode: false
+        schema:
+          type: array
+          items:
+            type: string
+      responses:
+        200:
+          description: successful operation
+          content:
+            application/xml:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Pet'
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/Pet'
+        400:
+          description: Invalid tag value
+      deprecated: true
+      security:
+      - petstore_auth:
+        - write:pets
+        - read:pets
+      x-accepts: application/json
+      x-tags:
+      - tag: pet
+  /pet/{petId}:
+    get:
+      tags:
+      - pet
+      summary: Find pet by ID
+      description: Returns a single pet
+      operationId: getPetById
+      parameters:
+      - name: petId
+        in: path
+        description: ID of pet to return
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: integer
+          format: int64
+      responses:
+        200:
+          description: successful operation
+          content:
+            application/xml:
+              schema:
+                $ref: '#/components/schemas/Pet'
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Pet'
+        400:
+          description: Invalid ID supplied
+        404:
+          description: Pet not found
+      security:
+      - api_key: []
+      x-accepts: application/json
+      x-tags:
+      - tag: pet
+    post:
+      tags:
+      - pet
+      summary: Updates a pet in the store with form data
+      operationId: updatePetWithForm
+      parameters:
+      - name: petId
+        in: path
+        description: ID of pet that needs to be updated
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: integer
+          format: int64
+      requestBody:
+        content:
+          application/x-www-form-urlencoded:
+            schema:
+              $ref: '#/components/schemas/body'
+      responses:
+        405:
+          description: Invalid input
+      security:
+      - petstore_auth:
+        - write:pets
+        - read:pets
+      x-accepts: application/json
+      x-tags:
+      - tag: pet
+      x-contentType: application/x-www-form-urlencoded
+    delete:
+      tags:
+      - pet
+      summary: Deletes a pet
+      operationId: deletePet
+      parameters:
+      - name: api_key
+        in: header
+        required: false
+        style: simple
+        explode: false
+        schema:
+          type: string
+      - name: petId
+        in: path
+        description: Pet id to delete
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: integer
+          format: int64
+      responses:
+        400:
+          description: Invalid pet value
+      security:
+      - petstore_auth:
+        - write:pets
+        - read:pets
+      x-accepts: application/json
+      x-tags:
+      - tag: pet
+  /pet/{petId}/uploadImage:
+    post:
+      tags:
+      - pet
+      summary: uploads an image
+      operationId: uploadFile
+      parameters:
+      - name: petId
+        in: path
+        description: ID of pet to update
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: integer
+          format: int64
+      requestBody:
+        content:
+          multipart/form-data:
+            schema:
+              $ref: '#/components/schemas/body_1'
+      responses:
+        200:
+          description: successful operation
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ApiResponse'
+      security:
+      - petstore_auth:
+        - write:pets
+        - read:pets
+      x-accepts: application/json
+      x-tags:
+      - tag: pet
+      x-contentType: multipart/form-data
+  /store/inventory:
+    get:
+      tags:
+      - store
+      summary: Returns pet inventories by status
+      description: Returns a map of status codes to quantities
+      operationId: getInventory
+      responses:
+        200:
+          description: successful operation
+          content:
+            application/json:
+              schema:
+                type: object
+                additionalProperties:
+                  type: integer
+                  format: int32
+      security:
+      - api_key: []
+      x-accepts: application/json
+      x-tags:
+      - tag: store
+  /store/order:
+    post:
+      tags:
+      - store
+      summary: Place an order for a pet
+      operationId: placeOrder
+      requestBody:
+        description: order placed for purchasing the pet
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Order'
+        required: true
+      responses:
+        200:
+          description: successful operation
+          content:
+            application/xml:
+              schema:
+                $ref: '#/components/schemas/Order'
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Order'
+        400:
+          description: Invalid Order
+      x-accepts: application/json
+      x-tags:
+      - tag: store
+      x-contentType: application/json
+  /store/order/{orderId}:
+    get:
+      tags:
+      - store
+      summary: Find purchase order by ID
+      description: For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions
+      operationId: getOrderById
+      parameters:
+      - name: orderId
+        in: path
+        description: ID of pet that needs to be fetched
+        required: true
+        style: simple
+        explode: false
+        schema:
+          maximum: 5
+          minimum: 1
+          type: integer
+          format: int64
+      responses:
+        200:
+          description: successful operation
+          content:
+            application/xml:
+              schema:
+                $ref: '#/components/schemas/Order'
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Order'
+        400:
+          description: Invalid ID supplied
+        404:
+          description: Order not found
+      x-accepts: application/json
+      x-tags:
+      - tag: store
+    delete:
+      tags:
+      - store
+      summary: Delete purchase order by ID
+      description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
+      operationId: deleteOrder
+      parameters:
+      - name: orderId
+        in: path
+        description: ID of the order that needs to be deleted
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        400:
+          description: Invalid ID supplied
+        404:
+          description: Order not found
+      x-accepts: application/json
+      x-tags:
+      - tag: store
+  /user:
+    post:
+      tags:
+      - user
+      summary: Create user
+      description: This can only be done by the logged in user.
+      operationId: createUser
+      requestBody:
+        description: Created user object
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/User'
+        required: true
+      responses:
+        default:
+          description: successful operation
+      x-accepts: application/json
+      x-tags:
+      - tag: user
+      x-contentType: application/json
+  /user/createWithArray:
+    post:
+      tags:
+      - user
+      summary: Creates list of users with given input array
+      operationId: createUsersWithArrayInput
+      requestBody:
+        $ref: '#/components/requestBodies/UserArray'
+      responses:
+        default:
+          description: successful operation
+      x-accepts: application/json
+      x-tags:
+      - tag: user
+      x-contentType: application/json
+  /user/createWithList:
+    post:
+      tags:
+      - user
+      summary: Creates list of users with given input array
+      operationId: createUsersWithListInput
+      requestBody:
+        $ref: '#/components/requestBodies/UserArray'
+      responses:
+        default:
+          description: successful operation
+      x-accepts: application/json
+      x-tags:
+      - tag: user
+      x-contentType: application/json
+  /user/login:
+    get:
+      tags:
+      - user
+      summary: Logs user into the system
+      operationId: loginUser
+      parameters:
+      - name: username
+        in: query
+        description: The user name for login
+        required: true
+        style: form
+        explode: true
+        schema:
+          type: string
+      - name: password
+        in: query
+        description: The password for login in clear text
+        required: true
+        style: form
+        explode: true
+        schema:
+          type: string
+      responses:
+        200:
+          description: successful operation
+          headers:
+            X-Rate-Limit:
+              description: calls per hour allowed by the user
+              style: simple
+              explode: false
+              schema:
+                type: integer
+                format: int32
+            X-Expires-After:
+              description: date in UTC when toekn expires
+              style: simple
+              explode: false
+              schema:
+                type: string
+                format: date-time
+          content:
+            application/xml:
+              schema:
+                type: string
+            application/json:
+              schema:
+                type: string
+        400:
+          description: Invalid username/password supplied
+      x-accepts: application/json
+      x-tags:
+      - tag: user
+  /user/logout:
+    get:
+      tags:
+      - user
+      summary: Logs out current logged in user session
+      operationId: logoutUser
+      responses:
+        default:
+          description: successful operation
+      x-accepts: application/json
+      x-tags:
+      - tag: user
+  /user/{username}:
+    get:
+      tags:
+      - user
+      summary: Get user by user name
+      operationId: getUserByName
+      parameters:
+      - name: username
+        in: path
+        description: The name that needs to be fetched. Use user1 for testing.
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        200:
+          description: successful operation
+          content:
+            application/xml:
+              schema:
+                $ref: '#/components/schemas/User'
+            application/json:
+              schema:
+                $ref: '#/components/schemas/User'
+        400:
+          description: Invalid username supplied
+        404:
+          description: User not found
+      x-accepts: application/json
+      x-tags:
+      - tag: user
+    put:
+      tags:
+      - user
+      summary: Updated user
+      description: This can only be done by the logged in user.
+      operationId: updateUser
+      parameters:
+      - name: username
+        in: path
+        description: name that need to be deleted
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      requestBody:
+        description: Updated user object
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/User'
+        required: true
+      responses:
+        400:
+          description: Invalid user supplied
+        404:
+          description: User not found
+      x-accepts: application/json
+      x-tags:
+      - tag: user
+      x-contentType: application/json
+    delete:
+      tags:
+      - user
+      summary: Delete user
+      description: This can only be done by the logged in user.
+      operationId: deleteUser
+      parameters:
+      - name: username
+        in: path
+        description: The name that needs to be deleted
+        required: true
+        style: simple
+        explode: false
+        schema:
+          type: string
+      responses:
+        400:
+          description: Invalid username supplied
+        404:
+          description: User not found
+      x-accepts: application/json
+      x-tags:
+      - tag: user
+components:
+  schemas:
+    Order:
+      title: Pet Order
+      type: object
+      properties:
+        id:
+          type: integer
+          format: int64
+        petId:
+          type: integer
+          format: int64
+        quantity:
+          type: integer
+          format: int32
+        shipDate:
+          type: string
+          format: date-time
+        status:
+          type: string
+          description: Order Status
+          enum:
+          - placed
+          - approved
+          - delivered
+        complete:
+          type: boolean
+          default: false
+      description: An order for a pets from the pet store
+      example:
+        petId: 6
+        quantity: 1
+        id: 0
+        shipDate: 2000-01-23T04:56:07.000+00:00
+        complete: false
+        status: placed
+      xml:
+        name: Order
+    Category:
+      title: Pet category
+      type: object
+      properties:
+        id:
+          type: integer
+          format: int64
+        name:
+          type: string
+      description: A category for a pet
+      example:
+        name: name
+        id: 6
+      xml:
+        name: Category
+    User:
+      title: a User
+      type: object
+      properties:
+        id:
+          type: integer
+          format: int64
+        username:
+          type: string
+        firstName:
+          type: string
+        lastName:
+          type: string
+        email:
+          type: string
+        password:
+          type: string
+        phone:
+          type: string
+        userStatus:
+          type: integer
+          description: User Status
+          format: int32
+      description: A User who is purchasing from the pet store
+      example:
+        firstName: firstName
+        lastName: lastName
+        password: password
+        userStatus: 6
+        phone: phone
+        id: 0
+        email: email
+        username: username
+      xml:
+        name: User
+    Tag:
+      title: Pet Tag
+      type: object
+      properties:
+        id:
+          type: integer
+          format: int64
+        name:
+          type: string
+      description: A tag for a pet
+      example:
+        name: name
+        id: 1
+      xml:
+        name: Tag
+    Pet:
+      title: a Pet
+      required:
+      - name
+      - photoUrls
+      type: object
+      properties:
+        id:
+          type: integer
+          format: int64
+        category:
+          $ref: '#/components/schemas/Category'
+        name:
+          type: string
+          example: doggie
+        photoUrls:
+          type: array
+          xml:
+            name: photoUrl
+            wrapped: true
+          items:
+            type: string
+        tags:
+          type: array
+          xml:
+            name: tag
+            wrapped: true
+          items:
+            $ref: '#/components/schemas/Tag'
+        status:
+          type: string
+          description: pet status in the store
+          enum:
+          - available
+          - pending
+          - sold
+      description: A pet for sale in the pet store
+      example:
+        photoUrls:
+        - photoUrls
+        - photoUrls
+        name: doggie
+        id: 0
+        category:
+          name: name
+          id: 6
+        tags:
+        - name: name
+          id: 1
+        - name: name
+          id: 1
+        status: available
+      xml:
+        name: Pet
+    ApiResponse:
+      title: An uploaded response
+      type: object
+      properties:
+        code:
+          type: integer
+          format: int32
+        type:
+          type: string
+        message:
+          type: string
+      description: Describes the result of uploading an image resource
+      example:
+        code: 0
+        type: type
+        message: message
+    body:
+      type: object
+      properties:
+        name:
+          type: string
+          description: Updated name of the pet
+        status:
+          type: string
+          description: Updated status of the pet
+    body_1:
+      type: object
+      properties:
+        additionalMetadata:
+          type: string
+          description: Additional data to pass to server
+        file:
+          type: string
+          description: file to upload
+          format: binary
+  requestBodies:
+    UserArray:
+      description: List of user object
+      content:
+        application/json:
+          schema:
+            type: array
+            items:
+              $ref: '#/components/schemas/User'
+      required: true
+    Pet:
+      description: Pet object that needs to be added to the store
+      content:
+        application/json:
+          schema:
+            $ref: '#/components/schemas/Pet'
+        application/xml:
+          schema:
+            $ref: '#/components/schemas/Pet'
+      required: true
+  securitySchemes:
+    petstore_auth:
+      type: oauth2
+      flows:
+        implicit:
+          authorizationUrl: /api/oauth/dialog
+          scopes:
+            write:pets: modify pets in your account
+            read:pets: read your pets
+    api_key:
+      type: apiKey
+      name: api_key
+      in: header
\ No newline at end of file
diff --git a/spotbugs-exclude.xml b/spotbugs-exclude.xml
new file mode 100644
index 0000000..28895ee
--- /dev/null
+++ b/spotbugs-exclude.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<FindBugsFilter
+   xmlns="https://github.com/spotbugs/filter/3.0.0"
+   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+   xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
+   <Match>
+     <!-- Ignore all generated classes -->
+     <Or>
+       <Package name="~com\.apigee\.security\.oas\..*" />
+       <Package name="com.apigee.security.oas" />
+     </Or>
+   </Match>
+   <Match>
+     <Or>
+       <!-- Does not play well with @Setup methods. -->
+       <Bug pattern="SE_TRANSIENT_FIELD_NOT_RESTORED" />
+
+       <!-- This pattern is not really appropriate in Dataflow
+       or when working with annotation processors. -->
+       <Bug pattern="UMAC_UNCALLABLE_METHOD_OF_ANONYMOUS_CLASS" />
+
+       <!-- Relying on errorprone's instead as it's more robust. -->
+       <Bug pattern="NP_NONNULL_RETURN_VIOLATION" />
+
+       <!-- This does not play nicely with the google-style builders. -->
+       <Bug pattern="RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE" />
+     </Or>
+   </Match>
+   <Match>
+     <!-- Within Dataflow Projects -->
+     <Package name="~com\.apigee\.security\.oas\..*" />
+     <Or>
+       <!-- Spotbugs thinks Serializable Functions are inner classes and gets very confused. -->
+       <Bug pattern="SE_INNER_CLASS" />
+       <Bug pattern="SIC_INNER_SHOULD_BE_STATIC_ANON" />
+     </Or>
+   </Match>
+   <Match>
+     <!-- Ignore matches in test classes except for those relating to tests. -->
+     <Class name="~.*\.*Test" />
+     <Not>
+       <Bug code="IJU" />
+     </Not>
+   </Match>
+</FindBugsFilter>
\ No newline at end of file