001/*
002 * Configurate
003 * Copyright (C) zml and Configurate contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.spongepowered.configurate.loader;
018
019import org.checkerframework.checker.nullness.qual.Nullable;
020import org.spongepowered.configurate.ConfigurateException;
021import org.spongepowered.configurate.ConfigurationNode;
022
023import java.io.IOException;
024import java.util.Arrays;
025
026/**
027 * Indicates an error that occurred while parsing the configuration.
028 *
029 * <p>These exceptions can include a specific position reference
030 * within a file.</p>
031 *
032 * @since 4.0.0
033 */
034public class ParsingException extends ConfigurateException {
035
036    /**
037     * Indicates a line or column is unknown.
038     */
039    public static final int UNKNOWN_POS = -1;
040
041    private static final char POSITION_MARKER = '^';
042    private static final long serialVersionUID = 8379206111053602577L;
043
044    /**
045     * Given an unknown {@link IOException}, return it as a Configurate type.
046     *
047     * <p>If the input {@code ex} is already a {@link ParsingException},
048     * this method returns the input value.</p>
049     *
050     * @param source node where the source exception was thrown
051     * @param ex the source exception
052     * @return an exception, either casted or wrapped
053     * @since 4.0.0
054     */
055    public static ParsingException wrap(final ConfigurationNode source, final IOException ex) {
056        if (ex instanceof ParsingException) {
057            return (ParsingException) ex;
058        } else {
059            return new ParsingException(source, -1, -1, null, null, ex);
060        }
061    }
062
063    private final int line;
064    private final int column;
065    private final @Nullable String context;
066
067    /**
068     * Create a new parsing exception.
069     *
070     * @param position position in the node structure where the error occurred
071     * @param line line with issue
072     * @param column column in the line
073     * @param context the line in a file where the error occurred
074     * @param message message describing the error
075     * @since 4.0.0
076     */
077    public ParsingException(
078        final ConfigurationNode position,
079        final int line,
080        final int column,
081        final String context,
082        final @Nullable String message
083    ) {
084        this(position, line, column, context, message, null);
085    }
086
087    /**
088     * Create a new parsing exception.
089     *
090     * @param line line with issue
091     * @param column column in the line
092     * @param context the line in a file where the error occurred
093     * @param message message describing the error
094     * @param cause direct cause
095     * @since 4.0.0
096     */
097    public ParsingException(
098        final int line,
099        final int column,
100        final @Nullable String context,
101        final @Nullable String message,
102        final @Nullable Throwable cause
103    ) {
104        super(message, cause);
105        this.line = line;
106        this.column = column;
107        this.context = context;
108    }
109
110    /**
111     * Create a new parsing exception.
112     *
113     * @param position position in the node structure where the error occurred
114     * @param line line with issue
115     * @param column column in the line
116     * @param context the line in a file where the error occurred
117     * @param message message describing the error
118     * @param cause direct cause
119     * @since 4.0.0
120     */
121    public ParsingException(
122        final ConfigurationNode position,
123        final int line,
124        final int column,
125        final @Nullable String context,
126        final @Nullable String message,
127        final @Nullable Throwable cause
128    ) {
129        super(position, message, cause);
130        this.line = line;
131        this.column = column;
132        this.context = context;
133    }
134
135    /**
136     * Line most closely associated with this error.
137     *
138     * @return line, or {@code -1} for unknown
139     * @since 4.0.0
140     */
141    public int line() {
142        return this.line;
143    }
144
145    /**
146     * Column most closely associated with the error.
147     *
148     * @return column, or {@code -1} for unknown
149     * @since 4.0.0
150     */
151    public int column() {
152        return this.column;
153    }
154
155    /**
156     * A context line from the source, if available.
157     *
158     * @return context line
159     * @since 4.0.0
160     */
161    public @Nullable String context() {
162        return this.context;
163    }
164
165    @Override
166    public @Nullable String getMessage() {
167        if (this.line == UNKNOWN_POS || this.column == UNKNOWN_POS) {
168            return super.getMessage();
169        }
170        final @Nullable String rawMessage = this.rawMessage();
171        final StringBuilder message = new StringBuilder(rawMessage == null ? 0 : (rawMessage.length() + 20));
172        message.append(this.path())
173                .append("@(line ").append(this.line).append(", col ").append(this.column).append("): ")
174                .append(rawMessage);
175
176        if (this.context != null) {
177            message.append(System.lineSeparator()).append(this.context);
178            if (this.column >= 0 && this.column < this.context.length()) {
179                message.append(System.lineSeparator());
180                if (this.column > 0) {
181                    final char[] spaces = new char[this.column - 1];
182                    Arrays.fill(spaces, ' ');
183                    message.append(spaces);
184                }
185
186                message.append(POSITION_MARKER);
187            }
188        }
189
190        return message.toString();
191    }
192
193}