package io.dropwizard.configuration;
import com.fasterxml.jackson.core.JsonLocation;
import com.fasterxml.jackson.databind.JsonMappingException;
import io.dropwizard.util.Strings;
import org.apache.commons.text.similarity.LevenshteinDistance;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import static java.util.Objects.requireNonNull;
A ConfigurationException
for errors parsing a configuration file. /**
* A {@link ConfigurationException} for errors parsing a configuration file.
*/
public class ConfigurationParsingException extends ConfigurationException {
private static final long serialVersionUID = 1L;
static class Builder {
private static final int MAX_SUGGESTIONS = 5;
private String summary;
private String detail = "";
private List<JsonMappingException.Reference> fieldPath = Collections.emptyList();
private int line = -1;
private int column = -1;
@Nullable
private Exception cause;
private List<String> suggestions = new ArrayList<>();
@Nullable
private String suggestionBase;
private boolean suggestionsSorted = false;
Builder(String summary) {
this.summary = summary;
}
Returns a brief message summarizing the error.
Returns: a brief message summarizing the error.
/**
* Returns a brief message summarizing the error.
*
* @return a brief message summarizing the error.
*/
public String getSummary() {
return summary.trim();
}
Returns a detailed description of the error.
Returns: a detailed description of the error or the empty String if there is none.
/**
* Returns a detailed description of the error.
*
* @return a detailed description of the error or the empty String if there is none.
*/
public String getDetail() {
return detail.trim();
}
Determines if a detailed description of the error has been set.
Returns: true if there is a detailed description of the error; false if there is not.
/**
* Determines if a detailed description of the error has been set.
*
* @return true if there is a detailed description of the error; false if there is not.
*/
public boolean hasDetail() {
return detail != null && !detail.isEmpty();
}
Returns the path to the problematic JSON field, if there is one.
Returns: a List
with each element in the path in order, beginning at the root; or an empty list if there is no JSON field in the context of this error.
/**
* Returns the path to the problematic JSON field, if there is one.
*
* @return a {@link List} with each element in the path in order, beginning at the root; or
* an empty list if there is no JSON field in the context of this error.
*/
public List<JsonMappingException.Reference> getFieldPath() {
return fieldPath;
}
Determines if the path to a JSON field has been set.
Returns: true if the path to a JSON field has been set for the error; false if no path has
yet been set.
/**
* Determines if the path to a JSON field has been set.
*
* @return true if the path to a JSON field has been set for the error; false if no path has
* yet been set.
*/
public boolean hasFieldPath() {
return fieldPath != null && !fieldPath.isEmpty();
}
Returns the line number of the source of the problem.
Note: the line number is indexed from zero.
Returns: the line number of the source of the problem, or -1 if unknown.
/**
* Returns the line number of the source of the problem.
* <p/>
* Note: the line number is indexed from zero.
*
* @return the line number of the source of the problem, or -1 if unknown.
*/
public int getLine() {
return line;
}
Returns the column number of the source of the problem.
Note: the column number is indexed from zero.
Returns: the column number of the source of the problem, or -1 if unknown.
/**
* Returns the column number of the source of the problem.
* <p/>
* Note: the column number is indexed from zero.
*
* @return the column number of the source of the problem, or -1 if unknown.
*/
public int getColumn() {
return column;
}
Determines if a location (line and column numbers) have been set.
Returns: true if both a line and column number has been set; false if only one or neither
have been set.
/**
* Determines if a location (line and column numbers) have been set.
*
* @return true if both a line and column number has been set; false if only one or neither
* have been set.
*/
public boolean hasLocation() {
return line > -1 && column > -1;
}
Returns a list of suggestions.
If a suggestion-base
has been set, the suggestions will be sorted according to the suggestion-base such that suggestions close to the base appear first in the list. Returns: a list of suggestions, or the empty list if there are no suggestions available.
/**
* Returns a list of suggestions.
* <p/>
* If a {@link #getSuggestionBase() suggestion-base} has been set, the suggestions will be
* sorted according to the suggestion-base such that suggestions close to the base appear
* first in the list.
*
* @return a list of suggestions, or the empty list if there are no suggestions available.
*/
public List<String> getSuggestions() {
if (suggestionsSorted || !hasSuggestionBase()) {
return suggestions;
}
suggestions.sort(new LevenshteinComparator(requireNonNull(getSuggestionBase())));
suggestionsSorted = true;
return suggestions;
}
Determines whether suggestions are available.
Returns: true if suggestions are available; false if they are not.
/**
* Determines whether suggestions are available.
*
* @return true if suggestions are available; false if they are not.
*/
public boolean hasSuggestions() {
return suggestions != null && !suggestions.isEmpty();
}
Returns the base for ordering suggestions.
Suggestions will be ordered such that suggestions closer to the base will appear first.
Returns: the base for suggestions.
/**
* Returns the base for ordering suggestions.
* <p/>
* Suggestions will be ordered such that suggestions closer to the base will appear first.
*
* @return the base for suggestions.
*/
@Nullable
public String getSuggestionBase() {
return suggestionBase;
}
Determines whether a suggestion base is available.
If no base is available, suggestions will not be sorted.
Returns: true if a base is available for suggestions; false if there is none.
/**
* Determines whether a suggestion base is available.
* <p/>
* If no base is available, suggestions will not be sorted.
*
* @return true if a base is available for suggestions; false if there is none.
*/
public boolean hasSuggestionBase() {
return suggestionBase != null && !suggestionBase.isEmpty();
}
Returns the Exception
that encapsulates the problem itself. Returns: an Exception representing the cause of the problem, or null if there is none.
/**
* Returns the {@link Exception} that encapsulates the problem itself.
*
* @return an Exception representing the cause of the problem, or null if there is none.
*/
@Nullable
public Exception getCause() {
return cause;
}
Determines whether a cause has been set.
Returns: true if there is a cause; false if there is none.
/**
* Determines whether a cause has been set.
*
* @return true if there is a cause; false if there is none.
*/
public boolean hasCause() {
return cause != null;
}
Builder setCause(Exception cause) {
this.cause = cause;
return this;
}
Builder setDetail(@Nullable String detail) {
this.detail = Strings.nullToEmpty(detail);
return this;
}
Builder setFieldPath(List<JsonMappingException.Reference> fieldPath) {
this.fieldPath = fieldPath;
return this;
}
Builder setLocation(JsonLocation location) {
return location == null
? this
: setLocation(location.getLineNr(), location.getColumnNr());
}
Builder setLocation(int line, int column) {
this.line = line;
this.column = column;
return this;
}
Builder addSuggestion(String suggestion) {
this.suggestionsSorted = false;
this.suggestions.add(suggestion);
return this;
}
Builder addSuggestions(Collection<String> suggestions) {
this.suggestionsSorted = false;
this.suggestions.addAll(suggestions);
return this;
}
Builder setSuggestionBase(String base) {
this.suggestionBase = base;
this.suggestionsSorted = false;
return this;
}
ConfigurationParsingException build(String path) {
final StringBuilder sb = new StringBuilder(getSummary());
if (hasFieldPath()) {
sb.append(" at: ").append(buildPath(getFieldPath()));
} else if (hasLocation()) {
sb.append(" at line: ").append(getLine() + 1)
.append(", column: ").append(getColumn() + 1);
}
if (hasDetail()) {
sb.append("; ").append(getDetail());
}
if (hasSuggestions()) {
final List<String> suggestions = getSuggestions();
sb.append(NEWLINE).append(" Did you mean?:").append(NEWLINE);
final Iterator<String> it = suggestions.iterator();
int i = 0;
while (it.hasNext() && i < MAX_SUGGESTIONS) {
sb.append(" - ").append(it.next());
i++;
if (it.hasNext()) {
sb.append(NEWLINE);
}
}
final int total = suggestions.size();
if (i < total) {
sb.append(" [").append(total - i).append(" more]");
}
}
return hasCause()
? new ConfigurationParsingException(path, sb.toString(), requireNonNull(getCause()))
: new ConfigurationParsingException(path, sb.toString());
}
private String buildPath(Iterable<JsonMappingException.Reference> path) {
final StringBuilder sb = new StringBuilder();
if (path != null) {
final Iterator<JsonMappingException.Reference> it = path.iterator();
while (it.hasNext()) {
final JsonMappingException.Reference reference = it.next();
final String name = reference.getFieldName();
// append either the field name or list index
if (name == null) {
sb.append('[').append(reference.getIndex()).append(']');
} else {
sb.append(name);
}
if (it.hasNext()) {
sb.append('.');
}
}
}
return sb.toString();
}
protected static class LevenshteinComparator implements Comparator<String>, Serializable {
private static final long serialVersionUID = 1L;
private static final LevenshteinDistance LEVENSHTEIN_DISTANCE = new LevenshteinDistance();
private String base;
public LevenshteinComparator(String base) {
this.base = base;
}
Compares two Strings with respect to the base String, by Levenshtein distance.
The input that is the closest match to the base String will sort before the other.
Params: - a – an input to compare relative to the base.
- b – an input to compare relative to the base.
Returns: -1 if a
is closer to the base than b
; 1 if b
is closer to the base than a
; 0 if both a
and b
are equally close to the base.
/**
* Compares two Strings with respect to the base String, by Levenshtein distance.
* <p/>
* The input that is the closest match to the base String will sort before the other.
*
* @param a an input to compare relative to the base.
* @param b an input to compare relative to the base.
*
* @return -1 if {@code a} is closer to the base than {@code b}; 1 if {@code b} is
* closer to the base than {@code a}; 0 if both {@code a} and {@code b} are
* equally close to the base.
*/
@Override
public int compare(String a, String b) {
// shortcuts
if (a.equals(b)) {
return 0; // comparing the same value; don't bother
} else if (a.equals(base)) {
return -1; // a is equal to the base, so it's always first
} else if (b.equals(base)) {
return 1; // b is equal to the base, so it's always first
}
// determine which of the two is closer to the base and order it first
return Integer.compare(LEVENSHTEIN_DISTANCE.apply(a, base),
LEVENSHTEIN_DISTANCE.apply(b, base));
}
private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
}
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
}
}
}
Create a mutable Builder
to incrementally build a ConfigurationParsingException
. Params: - brief – the brief summary of the error.
Returns: a mutable builder to incrementally build a ConfigurationParsingException
.
/**
* Create a mutable {@link Builder} to incrementally build a {@link ConfigurationParsingException}.
*
* @param brief the brief summary of the error.
*
* @return a mutable builder to incrementally build a {@link ConfigurationParsingException}.
*/
static Builder builder(String brief) {
return new Builder(brief);
}
Creates a new ConfigurationParsingException for the given path with the given error.
Params: - path – the bad configuration path
- msg – the full error message
/**
* Creates a new ConfigurationParsingException for the given path with the given error.
*
* @param path the bad configuration path
* @param msg the full error message
*/
private ConfigurationParsingException(String path, String msg) {
super(path, Collections.singleton(msg));
}
Creates a new ConfigurationParsingException for the given path with the given error.
Params: - path – the bad configuration path
- msg – the full error message
- cause – the cause of the parsing error.
/**
* Creates a new ConfigurationParsingException for the given path with the given error.
*
* @param path the bad configuration path
* @param msg the full error message
* @param cause the cause of the parsing error.
*/
private ConfigurationParsingException(String path, String msg, Throwable cause) {
super(path, Collections.singleton(msg), cause);
}
}