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); } }