Created
December 5, 2016 23:43
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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