Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.github.mivek.exception;

/**
* @author mivek
*/
public enum ParseErrorType {
/** Station parse error. */
STATION,
/** Delivery time parse error. */
DELIVERY_TIME,
/** Validity parse error. */
VALIDITY,
/** Temperature parse error. */
TEMPERATURE,
/** Trend header parse error. */
TREND,
/** Generic / unknown parse error. */
INVALID_MESSAGE
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,36 @@ public class ParseException extends Exception {
private static final long serialVersionUID = -9022781702124062628L;
/** The error code. */
private final ErrorCodes codes;
/** The error type, may be null. */
private final ParseErrorType type;
/** The offending token, may be null. */
private final String offendingToken;
/** The token index in the tokenized input. */
private final int position;

/**
* Constructor with error code.
*
* @param code the error code.
*/
public ParseException(final ErrorCodes code) {
super(code.toString());
this(code, null, null, -1);
}

/**
* Constructor with error code and structured metadata.
*
* @param code the error code.
* @param type the parse error type.
* @param offendingToken the token that caused the failure.
* @param position the token index in the tokenized input.
*/
public ParseException(final ErrorCodes code, final ParseErrorType type, final String offendingToken, final int position) {
super(offendingToken == null ? code.toString() : code.toString() + " [" + offendingToken + "]");
this.codes = code;
this.type = type;
this.offendingToken = offendingToken;
this.position = position;
}

/**
Expand All @@ -28,4 +49,25 @@ public ParseException(final ErrorCodes code) {
public ErrorCodes getErrorCode() {
return codes;
}

/**
* @return the error type, or null if not set.
*/
public ParseErrorType getType() {
return type;
}

/**
* @return the offending token, or null if not set.
*/
public String getOffendingToken() {
return offendingToken;
}

/**
* @return the token index in the tokenized input.
*/
public int getPosition() {
return position;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import io.github.mivek.command.AirportSupplier;
import io.github.mivek.command.common.CommonCommandSupplier;
import io.github.mivek.enums.Flag;
import io.github.mivek.exception.ErrorCodes;
import io.github.mivek.exception.ParseErrorType;
import io.github.mivek.exception.ParseException;
import io.github.mivek.model.AbstractWeatherCode;
import java.time.LocalTime;
import java.util.Objects;
Expand Down Expand Up @@ -46,21 +49,27 @@ public abstract class AbstractWeatherCodeParser<T extends AbstractWeatherCode> e
*
* @param weatherCode The weather code.
* @param time the string to parse.
* @param position the token index in the tokenized input.
* @return true if the token was a real delivery time, false if it was a validity time.
* @throws ParseException if the time token is malformed.
*/
boolean parseDeliveryTime(final AbstractWeatherCode weatherCode, final String time) {
weatherCode.setDay(Integer.parseInt(time.substring(0, 2)));
int hours = Integer.parseInt(time.substring(2, 4));
LocalTime t;
if (time.length() > 6 && time.contains("/")) {
t = LocalTime.of(hours, 0);
weatherCode.setTime(t);
return false;
} else {
int minutes = Integer.parseInt(time.substring(4, 6));
t = LocalTime.of(hours, minutes);
weatherCode.setTime(t);
return true;
boolean parseDeliveryTime(final AbstractWeatherCode weatherCode, final String time, final int position) throws ParseException {
try {
weatherCode.setDay(Integer.parseInt(time.substring(0, 2)));
int hours = Integer.parseInt(time.substring(2, 4));
LocalTime t;
if (time.length() > 6 && time.contains("/")) {
t = LocalTime.of(hours, 0);
weatherCode.setTime(t);
return false;
} else {
int minutes = Integer.parseInt(time.substring(4, 6));
t = LocalTime.of(hours, minutes);
weatherCode.setTime(t);
return true;
}
} catch (NumberFormatException | StringIndexOutOfBoundsException e) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.DELIVERY_TIME, time, position);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
import io.github.mivek.command.metar.Command;
import io.github.mivek.command.metar.MetarCommandSupplier;
import io.github.mivek.enums.WeatherChangeType;
import io.github.mivek.exception.ErrorCodes;
import io.github.mivek.exception.ParseErrorType;
import io.github.mivek.exception.ParseException;
import io.github.mivek.factory.FactoryProvider;
import io.github.mivek.model.Airport;
import io.github.mivek.model.Metar;
import io.github.mivek.model.trend.MetarTrend;
import io.github.mivek.model.trend.validity.AbstractMetarTrendTime;
import io.github.mivek.utils.Converter;
import io.github.mivek.utils.Regex;
import java.util.Objects;
import java.util.regex.Pattern;

/**
* This controller contains methods that parse the metar code. This class is a
Expand All @@ -21,6 +24,8 @@
* @author mivek
*/
public final class MetarParser extends AbstractWeatherCodeParser<Metar> {
/** Station ICAO pattern. */
private static final Pattern STATION_PATTERN = Pattern.compile("^[A-Z]{4}$");
/** The command supplier. */
private final MetarCommandSupplier supplier;

Expand Down Expand Up @@ -56,9 +61,38 @@ public MetarParser(final CommonCommandSupplier commonCommandSupplier, final Rema
public Metar parse(final String code) throws ParseException {
Metar m = new Metar();
String[] metarTab = tokenize(code);
int startIndex = 0;
int index = parseIdentification(m, metarTab, code);
while (index < metarTab.length) {
if (!generalParse(m, metarTab[index]) && !parseFlags(m, metarTab[index])) {
if ("NOSIG".equals(metarTab[index])) {
m.setNosig(true);
} else if (RMK.equals(metarTab[index])) {
parseRMK(m, metarTab, index);
break;
} else if (metarTab[index].equals(TEMPO) || metarTab[index].equals(BECMG)) {
MetarTrend trend = new MetarTrend(WeatherChangeType.valueOf(metarTab[index]));
index = parseTrend(index, trend, metarTab);
m.addTrend(trend);
} else {
executeCommand(m, metarTab[index]);
}
}
index++;
}
return m;
}

// Check for METAR/SPECI prefix
/**
* Parses the report-type prefix, station and delivery time.
*
* @param m the metar being built.
* @param metarTab the tokenized message.
* @param code the raw message.
* @return the index of the first body token.
* @throws ParseException if the station or delivery time is missing or malformed.
*/
private int parseIdentification(final Metar m, final String[] metarTab, final String code) throws ParseException {
int startIndex = 0;
if (metarTab.length > 0) {
if ("METAR".equals(metarTab[0])) {
m.setReportType(io.github.mivek.enums.ReportType.METAR);
Expand All @@ -68,32 +102,21 @@ public Metar parse(final String code) throws ParseException {
startIndex = 1;
}
}

Airport airport = getAirportSupplier().get(metarTab[startIndex]);
m.setStation(metarTab[startIndex]);
m.setAirport(airport);
if (startIndex >= metarTab.length) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.STATION, null, startIndex);
}
String stationToken = metarTab[startIndex];
if (!Regex.match(STATION_PATTERN, stationToken)) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.STATION, stationToken, startIndex);
}
m.setStation(stationToken);
m.setAirport(getAirportSupplier().get(stationToken));
m.setMessage(code);
parseDeliveryTime(m, metarTab[startIndex + 1]);
int metarTabLength = metarTab.length;
int i = startIndex + 2;
while (i < metarTabLength) {
if (!generalParse(m, metarTab[i]) && !parseFlags(m, metarTab[i])) {
if ("NOSIG".equals(metarTab[i])) {
m.setNosig(true);
} else if (RMK.equals(metarTab[i])) {
parseRMK(m, metarTab, i);
break;
} else if (metarTab[i].equals(TEMPO) || metarTab[i].equals(BECMG)) {
MetarTrend trend = new MetarTrend(WeatherChangeType.valueOf(metarTab[i]));
i = parseTrend(i, trend, metarTab);
m.addTrend(trend);
} else {
executeCommand(m, metarTab[i]);
}
}
i++;
if (startIndex + 1 >= metarTab.length) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.DELIVERY_TIME, null, startIndex + 1);
}
return m;
parseDeliveryTime(m, metarTab[startIndex + 1], startIndex + 1);
return startIndex + 2;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.github.mivek.command.taf.Command;
import io.github.mivek.command.taf.TAFCommandSupplier;
import io.github.mivek.exception.ErrorCodes;
import io.github.mivek.exception.ParseErrorType;
import io.github.mivek.exception.ParseException;
import io.github.mivek.factory.FactoryProvider;
import io.github.mivek.model.Airport;
Expand All @@ -13,8 +14,10 @@
import io.github.mivek.model.trend.AbstractTafTrend;
import io.github.mivek.model.trend.validity.AbstractValidity;
import io.github.mivek.utils.Converter;
import io.github.mivek.utils.Regex;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Stream;

/**
Expand All @@ -27,6 +30,8 @@ public final class TAFParser extends AbstractWeatherCodeParser<TAF> {
private static final String TX = "TX";
/** Temperature Minimum Constant. */
private static final String TN = "TN";
/** Temperature regex pattern. */
private static final Pattern TEMPERATURE_REGEX = Pattern.compile("^(TX|TN)M?\\d{2}/\\d{4}Z$");
/** TAF command supplier. */
private final TAFCommandSupplier supplier;

Expand Down Expand Up @@ -75,11 +80,15 @@ public TAF parse(final String code) throws ParseException {
taf.setAirport(airport);
taf.setMessage(code);
// Day and time
if(parseDeliveryTime(taf, line1parts[i])) {
if(parseDeliveryTime(taf, line1parts[i], i)) {
i++;
}
// Validity Time
taf.setValidity(parseValidity(line1parts[i]));
String validityToken = line1parts[i];
if (!Regex.match(VALIDITY_REGEX, validityToken)) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.VALIDITY, validityToken, i);
}
taf.setValidity(parseValidity(validityToken));

// Handle rest of second line.
for (int j = i + 1; j < line1parts.length; j++) {
Expand All @@ -88,9 +97,9 @@ public TAF parse(final String code) throws ParseException {
parseRMK(taf, line1parts, j);
break;
} else if (part.startsWith(TX)) {
taf.setMaxTemperature(parseTemperature(part));
taf.setMaxTemperature(parseTemperature(part, j));
} else if (part.startsWith(TN)) {
taf.setMinTemperature(parseTemperature(part));
taf.setMinTemperature(parseTemperature(part, j));
} else {
executeCommand(taf, part);
}
Expand Down Expand Up @@ -149,8 +158,14 @@ private String[][] extractLineTokens(final String tafCode) {
* @param taf the TAF object to build
* @param parts the token of the line
*/
private void processLines(final TAF taf, final String[] parts) throws ParseException {
void processLines(final TAF taf, final String[] parts) throws ParseException {
if (parts[0].length() < 2) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.TREND, parts[0], 0);
}
AbstractTAFTrendParser<? extends AbstractValidity> trendParser = FactoryProvider.getTrendParser().create(parts[0].substring(0, 2));
if (trendParser == null) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.TREND, parts[0], 0);
}
AbstractTafTrend<? extends AbstractValidity> trend = trendParser.parse(parts);
taf.addTrend(trend);
}
Expand All @@ -159,15 +174,24 @@ private void processLines(final TAF taf, final String[] parts) throws ParseExcep
* Parse the temperature.
*
* @param tempPart the string to parse.
* @param position the token index.
* @return a temperature with its date.
* @throws ParseException if the temperature token is malformed.
*/
TemperatureDated parseTemperature(final String tempPart) {
TemperatureDated temperature = new TemperatureDated();
String[] parts = tempPart.split("/");
temperature.setTemperature(Converter.convertTemperature(parts[0].substring(2)));
temperature.setDay(Integer.parseInt(parts[1].substring(0, 2)));
temperature.setHour(Integer.parseInt(parts[1].substring(2, 4)));
return temperature;
TemperatureDated parseTemperature(final String tempPart, final int position) throws ParseException {
if (!Regex.match(TEMPERATURE_REGEX, tempPart)) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.TEMPERATURE, tempPart, position);
}
try {
TemperatureDated temperature = new TemperatureDated();
String[] parts = tempPart.split("/");
temperature.setTemperature(Converter.convertTemperature(parts[0].substring(2)));
temperature.setDay(Integer.parseInt(parts[1].substring(0, 2)));
temperature.setHour(Integer.parseInt(parts[1].substring(2, 4)));
return temperature;
} catch (NumberFormatException | StringIndexOutOfBoundsException e) {
throw new ParseException(ErrorCodes.ERROR_CODE_INVALID_MESSAGE, ParseErrorType.TEMPERATURE, tempPart, position);
}
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.github.mivek.parser;

import io.github.mivek.enums.*;
import io.github.mivek.exception.ErrorCodes;
import io.github.mivek.exception.ParseErrorType;
import io.github.mivek.exception.ParseException;
import io.github.mivek.internationalization.Messages;
import io.github.mivek.model.*;
Expand Down Expand Up @@ -485,4 +487,24 @@ void testParseWithRecentRain() throws ParseException {
assertEquals(Phenomenon.RAIN, m.getWeatherConditions().get(0).getPhenomenons().get(0));
}

@Test
void testParseInvalidStation() {
ParseException e = assertThrows(ParseException.class, () -> parser.parse("METAR 12 150500Z 17005KT 6000"));
assertEquals(ParseErrorType.STATION, e.getType());
assertEquals("12", e.getOffendingToken());
assertEquals(1, e.getPosition());
}

@Test
void testParseEmptyMessageThrowsStationError() {
ParseException e = assertThrows(ParseException.class, () -> parser.parse("METAR"));
assertEquals(ParseErrorType.STATION, e.getType());
}

@Test
void testParseWithoutDeliveryTimeThrowsDeliveryTimeError() {
ParseException e = assertThrows(ParseException.class, () -> parser.parse("LFPG"));
assertEquals(ParseErrorType.DELIVERY_TIME, e.getType());
}

}
Loading
Loading