Skip to content

Instantly share code, notes, and snippets.

@tkareine
Created December 5, 2016 23:43
Show Gist options
  • Save tkareine/16ac2c2442132352977f77f0673470fd to your computer and use it in GitHub Desktop.
Save tkareine/16ac2c2442132352977f77f0673470fd to your computer and use it in GitHub Desktop.
An example of user input validation, handling legacy code that loves to use nulls and exceptions.
package org.tkareine.validation_example;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
/**
* Design considerations:
*
* <ul>
* <li>validate all user input, showing all failures</li>
* <li>integrate with legacy backend code that uses nulls and (checked)
* exceptions</li>
* </ul>
*
* Usage:
*
* <pre>
* $ mkdir -p target &&
* javac -d target src/org/tkareine/validation_example/BuilderExample.java &&
* java -ea -cp target org.tkareine.validation_example.BuilderExample
* </pre>
*/
public class BuilderExample {
public static void main(String[] args) {
// demonstrates dependency injection
UserInputValidation userInputValidation = new UserInputValidation(new SafeLegacyBackend(new LegacyBackend("jdbc:oracle:thin:@sunspark:1521:monolithic")));
testSuccess(userInputValidation);
testFailureWhenEmpty(userInputValidation);
testFailureWhenInvalid(userInputValidation);
testFailureWhenRoleNotFound(userInputValidation);
System.out.println("ok");
}
private static void testSuccess(UserInputValidation userInputValidation) {
assert(userInputValidation.builder()
.birthYear("1942")
.role("scientist")
.build()
.isSuccess());
}
private static void testFailureWhenEmpty(UserInputValidation userInputValidation) {
UserInputValidation.Result validationResult = userInputValidation.builder()
.birthYear("")
.role("")
.build();
assert(!validationResult.isSuccess());
assert(validationResult.getValidationFailures().size() == 2);
assert(validationResult.getValidationFailures().get("birthYear").equals("empty"));
assert(validationResult.getValidationFailures().get("role").equals("empty"));
}
private static void testFailureWhenInvalid(UserInputValidation userInputValidation) {
UserInputValidation.Result validationResult = userInputValidation.builder()
.birthYear("-1981")
.role("en")
.build();
assert(!validationResult.isSuccess());
assert(validationResult.getValidationFailures().size() == 2);
assert(validationResult.getValidationFailures().get("birthYear").equals("not a year: \"-1981\""));
assert(validationResult.getValidationFailures().get("role").equals("no such role: \"en\""));
}
private static void testFailureWhenRoleNotFound(UserInputValidation userInputValidation) {
UserInputValidation.Result validationResult = userInputValidation.builder()
.birthYear("1981")
.role("engineer")
.build();
assert(!validationResult.isSuccess());
assert(validationResult.getValidationFailures().size() == 1);
assert(validationResult.getValidationFailures().get("role").equals("no such role: \"engineer\""));
}
public static class UserInputValidation {
private final SafeLegacyBackend safeLegacyBackend;
public UserInputValidation(SafeLegacyBackend safeLegacyBackend) {
this.safeLegacyBackend = safeLegacyBackend;
}
public Builder builder() {
return new Builder();
}
public class Builder {
private final Map<String, String> failures = new LinkedHashMap<>();
public Builder birthYear(String input) {
if (!checkEmptyInput("birthYear", input)) {
return this;
}
int year;
try {
year = Integer.parseUnsignedInt(input);
} catch (NumberFormatException e) {
return putFailure("birthYear", "not a year: \"" + input + "\"");
}
if (year < 1900 || year > LocalDate.now().getYear()) {
return putFailure("birthYear", "impossible: " + input);
}
return this;
}
public Builder role(String input) {
if (!checkEmptyInput("role", input)) {
return this;
}
if (!safeLegacyBackend.parseRole(input).isPresent()) {
return putFailure("role", "no such role: \"" + input + "\"");
}
return this;
}
public Result build() {
return failures.isEmpty()
? Result.success()
: Result.failure(failures);
}
private boolean checkEmptyInput(String fieldName, String input) {
if (input.isEmpty()) {
putFailure(fieldName, "empty");
return false;
} else {
return true;
}
}
private Builder putFailure(String fieldName, String failure) {
failures.put(fieldName, failure);
return this;
}
}
interface Result {
boolean isSuccess();
Map<String, String> getValidationFailures();
static Result success() {
return Success.INSTANCE;
}
static Result failure(Map<String, String> validationFailures) {
return new Failure(validationFailures);
}
}
public final static class Success implements Result {
private static Success INSTANCE = new Success();
private Success() {} // hide
@Override
public boolean isSuccess() {
return true;
}
@Override
public Map<String, String> getValidationFailures() {
// if you call me, you have a bug in the calling side
throw new UnsupportedOperationException("getValidationFailures for success result");
}
}
public final static class Failure implements Result {
private final Map<String, String> failures;
private Failure(Map<String, String> failures) {
this.failures = failures;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public Map<String, String> getValidationFailures() {
return failures;
}
}
}
/**
* More reasonable interface to {@link LegacyBackend} which you design and
* implement to suit your needs.
*/
public static class SafeLegacyBackend {
private LegacyBackend legacyBackend;
public SafeLegacyBackend(LegacyBackend legacyBackend) {
this.legacyBackend = legacyBackend;
}
public Optional<LegacyBackend.Role> parseRole(String name) {
LegacyBackend.Role role;
try {
role = legacyBackend.getRole(name);
} catch (IllegalArgumentException ignored) {
// Catch only the exceptions that are due to input. Let severe
// exceptions, such as SQLException, to propagate up the call
// stack. Severe exceptions might be due to system failure or
// programming error -- usually there's nothing you can do about
// them in the app's logic, anyway.
//
// This is known as the Fail Fast Principle.
return Optional.empty();
} catch (SQLException e) {
// Wrap checked exception inside an unchecked exception in
// order to avoid tainting new code with legacy design choices.
throw new RuntimeException(e);
}
// Consider designing new code so that you avoid using `null`. That
// way you avoid null checks and you can be sure that a method
// signature always returns a value of the specified return type.
return Optional.ofNullable(role);
}
}
public static class LegacyBackend {
final Database database;
public LegacyBackend(String dbURL) {
database = Database.connect(dbURL);
}
public Role getRole(String name) throws SQLException {
if (name == null || name.length() < 3) {
throw new IllegalArgumentException("invalid role name");
}
return database.getRoleByName(name);
}
static class Database {
private Database() {} // hide
static Database connect(String dbURL) {
return new Database(/* DriverManager.getConnection(dbURL) */);
}
Role getRoleByName(String name) throws SQLException {
// fake implementation
return name.equals("scientist")
? new Role(1L, "scientist", 42)
: null; // not found
}
}
static class Role {
private final long id;
private final String name;
private final int karma;
public Role(long id, String name, int karma) {
this.id = id;
this.name = name;
this.karma = karma;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment