Skip to content

Commit ebd2dbd

Browse files
committed
Improved Unicode handling of Unicode pattern and throwing IllegalArgumentException for incorrect syntax
1 parent 9e24760 commit ebd2dbd

File tree

7 files changed

+133
-82
lines changed

7 files changed

+133
-82
lines changed

src/main/java/com/mapcode/Decoder.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
package com.mapcode;
1818

19-
import javax.annotation.Nonnull;
20-
2119
import org.slf4j.Logger;
2220
import org.slf4j.LoggerFactory;
2321

22+
import javax.annotation.Nonnull;
23+
2424
class Decoder {
2525
private static final Logger LOG = LoggerFactory.getLogger(Decoder.class);
2626

@@ -30,23 +30,23 @@ class Decoder {
3030

3131
@Nonnull
3232
static Point decode(@Nonnull final String argMapcode,
33-
@Nonnull final Territory argTerritory)
34-
throws UnknownMapcodeException {
33+
@Nonnull final Territory argTerritory)
34+
throws UnknownMapcodeException {
3535
LOG.trace("decode: mapcode={}, territory={}", argMapcode, argTerritory.name());
3636

3737
String mapcode = argMapcode;
3838
Territory territory = argTerritory;
3939

40-
// in case of error, result.isDefined() is false
40+
// In case of error, result.isDefined() is false.
4141
Point result = Point.undefined();
4242
String extrapostfix = "";
4343

4444
final int minpos = mapcode.indexOf('-');
4545
if (minpos > 0) {
46-
extrapostfix = decodeUTF16(mapcode.substring(minpos + 1).trim());
47-
if (extrapostfix.contains("Z")) {
48-
throw new UnknownMapcodeException("Invalid character Z");
49-
}
46+
extrapostfix = decodeUTF16(mapcode.substring(minpos + 1).trim());
47+
if (extrapostfix.contains("Z")) {
48+
throw new UnknownMapcodeException("Invalid character Z");
49+
}
5050
mapcode = mapcode.substring(0, minpos);
5151
}
5252

@@ -599,7 +599,13 @@ else if (voweled && decode_chars[(int) str.charAt(v)] > 9) {
599599
return str;
600600
}
601601

602-
private static String decodeUTF16(final String str) {
602+
/**
603+
* This method decodes a Unicode string to ASCII. Package private for access by other modules.
604+
*
605+
* @param str Unicode string.
606+
* @return ASCII string.
607+
*/
608+
static String decodeUTF16(final String str) {
603609
final StringBuilder asciibuf = new StringBuilder();
604610
for (int index = 0; index < str.length(); index++) {
605611
if (str.charAt(index) == '.') {

src/main/java/com/mapcode/Mapcode.java

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
/**
2525
* This class defines a single mapcode encoding result, including the mapcode itself and the
2626
* territory definition.
27+
*
28+
* Note that the constructor will throw an {@link IllegalArgumentException} if the syntax of the mapcode
29+
* is not correct. It does not throw an {@link com.mapcode.UnknownMapcodeException}, because the mapcode
30+
* is not checked for validity, other than its syntax.
2731
*/
2832
public final class Mapcode {
2933
@Nonnull private final String mapcodePrecision0;
@@ -149,15 +153,24 @@ public enum MapcodeFormatType {
149153
}
150154

151155
/**
152-
* These patterns and regular expressions are used for checking mapcode format strings.
156+
* These patterns and matchers are used internally in this module to match mapcodes. They are
157+
* provided as statics to only compile these patterns once.
158+
*/
159+
@Nonnull static final String REGEX_MAPCODE_FORMAT1 = "^[\\p{Alpha}\\p{Digit}]{2,5}+";
160+
@Nonnull static final String REGEX_MAPCODE_FORMAT2 = "[.][\\p{Alpha}\\p{Digit}]{2,5}+";
161+
@Nonnull static final String REGEX_MAPCODE_PRECISION = "[-][\\p{Alpha}\\p{Digit}&&[^zZ]]{1,2}+";
162+
163+
/**
164+
* This patterns/regular expressions is used for checking mapcode format strings.
153165
* They've been made pulkic to allow others to use the correct regular expressions as well.
154166
*/
155-
@Nonnull public static final String REGEX_MAPCODE_FORMAT =
156-
"^[a-zA-Z0-9]{2,5}?[.][a-zA-Z0-9]{2,5}?([-][a-zA-Z0-9]{1,2}?)?$";
157-
@Nonnull public static final String REGEX_MAPCODE_PRECISION = "[-][a-zA-Z0-9]{1,2}?$";
167+
@Nonnull public static final String REGEX_MAPCODE_FORMAT =
168+
REGEX_MAPCODE_FORMAT1 + REGEX_MAPCODE_FORMAT2 + '(' + REGEX_MAPCODE_PRECISION + ")?$";
158169

159-
@Nonnull public static final Pattern PATTERN_MAPCODE_FORMAT = Pattern.compile(REGEX_MAPCODE_FORMAT);
160-
@Nonnull public static final Pattern PATTERN_MAPCODE_PRECISION = Pattern.compile(REGEX_MAPCODE_PRECISION);
170+
@Nonnull private static final Pattern PATTERN_MAPCODE_FORMAT =
171+
Pattern.compile(REGEX_MAPCODE_FORMAT, Pattern.UNICODE_CHARACTER_CLASS);
172+
@Nonnull private static final Pattern PATTERN_MAPCODE_PRECISION =
173+
Pattern.compile(REGEX_MAPCODE_PRECISION, Pattern.UNICODE_CHARACTER_CLASS);
161174

162175
/**
163176
* This method return the mapcode type, given a mapcode string. If the mapcode string has an invalid
@@ -172,15 +185,21 @@ public enum MapcodeFormatType {
172185
*/
173186
@Nonnull
174187
public static MapcodeFormatType getMapcodeFormatType(@Nonnull final String mapcode) {
175-
final Matcher matcherMapcodeFormat = PATTERN_MAPCODE_FORMAT.matcher(mapcode);
176-
if (!matcherMapcodeFormat.matches()) {
188+
189+
// First, decode to ASCII.
190+
final String decodedMapcode = Decoder.decodeUTF16(mapcode);
191+
192+
// Syntax needs to be OK.
193+
if (!PATTERN_MAPCODE_FORMAT.matcher(decodedMapcode).matches()) {
177194
return MapcodeFormatType.MAPCODE_TYPE_INVALID;
178195
}
179-
final Matcher matcherMapcodePrecision = PATTERN_MAPCODE_PRECISION.matcher(mapcode);
196+
197+
// Precision part should be OK.
198+
final Matcher matcherMapcodePrecision = PATTERN_MAPCODE_PRECISION.matcher(decodedMapcode);
180199
if (!matcherMapcodePrecision.find()) {
181200
return MapcodeFormatType.MAPCODE_TYPE_PRECISION_0;
182201
}
183-
final int length = matcherMapcodePrecision.group().length();
202+
final int length = matcherMapcodePrecision.end() - matcherMapcodePrecision.start();
184203
assert (2 <= length) && (length <= 3);
185204
if (length == 2) {
186205
return MapcodeFormatType.MAPCODE_TYPE_PRECISION_1;

src/main/java/com/mapcode/MapcodeCodec.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,9 @@ public static Mapcode encodeToInternational(
194194
*
195195
* @param mapcode Mapcode.
196196
* @return Point corresponding to mapcode.
197-
* @throws UnknownMapcodeException Thrown if the mapcode cannot be decoded into a point.
198-
* @throws IllegalArgumentException Thrown if arguments are null.
197+
* @throws UnknownMapcodeException Thrown if the mapcode has the correct synaxt,
198+
* but cannot be decoded into a point.
199+
* @throws IllegalArgumentException Thrown if arguments are null, or if the syntax of the mapcode is incorrect.
199200
*/
200201
@Nonnull
201202
public static Point decode(
@@ -219,6 +220,10 @@ public static Point decode(
219220
else {
220221
territory = Territory.AAA;
221222
}
223+
if (!Mapcode.isValidMapcodeFormat(mapcodeTrimmed)) {
224+
throw new IllegalArgumentException(mapcode + " is not a correctly formatted mapcode; " +
225+
"the regular expression for the mapcode syntax is: " + Mapcode.REGEX_MAPCODE_FORMAT);
226+
}
222227
return decode(mapcodeTrimmed, territory);
223228
}
224229

@@ -231,8 +236,8 @@ public static Point decode(
231236
* @param mapcode Mapcode.
232237
* @param territoryContext Territory for disambiguation purposes.
233238
* @return Point corresponding to mapcode. Latitude range: -90..90, longitude range: -180..180.
234-
* @throws UnknownMapcodeException Thrown if the mapcode cannot be decoded into a point.
235-
* @throws IllegalArgumentException Thrown if arguments are null.
239+
* @throws UnknownMapcodeException Thrown if the mapcode has the right syntax, but cannot be decoded into a point.
240+
* @throws IllegalArgumentException Thrown if arguments are null, or if the syntax of the mapcode is incorrect.
236241
*/
237242
@Nonnull
238243
public static Point decode(
@@ -241,6 +246,11 @@ public static Point decode(
241246
checkNonnull("mapcode", mapcode);
242247
checkNonnull("territoryConext", territoryContext);
243248
final String mapcodeTrimmed = mapcode.trim();
249+
if (!Mapcode.isValidMapcodeFormat(mapcodeTrimmed)) {
250+
throw new IllegalArgumentException(mapcode + " is not a correctly formatted mapcode; " +
251+
"the regular expression for the mapcode syntax is: " + Mapcode.REGEX_MAPCODE_FORMAT);
252+
}
253+
244254
@Nonnull final Point point = Decoder.decode(mapcodeTrimmed, territoryContext);
245255
assert point != null;
246256

src/main/java/com/mapcode/UnknownMapcodeException.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
* Mapcode public interface.
2424
* ----------------------------------------------------------------------------------------------
2525
*
26-
* This exception is thrown for unknown mapcodes.
26+
* This exception is thrown for invalid mapcodes (which have the right syntax, are correctly formatted).
2727
*/
2828
public final class UnknownMapcodeException extends Exception {
2929
private static final long serialVersionUID = 1L;

src/site/apt/ReleaseNotes.apt.vm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Release Notes (Version ${project.version})
1616

1717
* Constructor of <<<Mapcode>>> now checks for validity of mapcode string.
1818

19+
* Added Unicode handling of high precision mapcodes and added check to throw an <<<IllegalArgumentException>>>
20+
if the character 'Z' or equivalent Unicode character is contained in the high precision part according to
21+
the Mapcode documenation.
22+
1923
[]
2024

2125
* 1.40.1

src/test/java/com/mapcode/DecoderTest.java

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616

1717
package com.mapcode;
1818

19-
import static org.junit.Assert.assertEquals;
20-
2119
import org.junit.Test;
2220
import org.slf4j.Logger;
2321
import org.slf4j.LoggerFactory;
2422

23+
import static org.junit.Assert.assertEquals;
24+
2525
@SuppressWarnings({"OverlyBroadThrowsClause", "ProhibitedExceptionDeclared"})
2626
public class DecoderTest {
2727
private static final Logger LOG = LoggerFactory.getLogger(DecoderTest.class);
@@ -60,126 +60,126 @@ public void highPrecisionTomTomOffice2() throws Exception {
6060

6161
@Test
6262
public void highPrecisionUnicodeAthensAcropolis1() throws Exception {
63-
LOG.info("highPrecisionUnicodeAthensAcropolis1");
64-
final Point point = MapcodeCodec.decode("ΗΠ.Θ2-Φ2", Territory.GRC);
65-
assertEquals("decodeUnicode latitude", 37971844, point.getLatMicroDeg());
66-
assertEquals("decodeUnicode longitude", 23726223,
67-
point.getLonMicroDeg());
63+
LOG.info("highPrecisionUnicodeAthensAcropolis1");
64+
final Point point = MapcodeCodec.decode("\u0397\u03a0.\u03982-\u03a62", Territory.GRC);
65+
assertEquals("decodeUnicode latitude", 37971844, point.getLatMicroDeg());
66+
assertEquals("decodeUnicode longitude", 23726223,
67+
point.getLonMicroDeg());
6868
}
6969

7070
@Test
7171
public void highPrecisionUnicodeAthensAcropolis2() throws Exception {
72-
LOG.info("highPrecisionUnicodeAthensAcropolis2");
73-
final Point point = MapcodeCodec.decode("GRC ΗΠ.Θ2-Φ2");
74-
assertEquals("decodeUnicode latitude", 37971844, point.getLatMicroDeg());
75-
assertEquals("decodeUnicode longitude", 23726223,
76-
point.getLonMicroDeg());
72+
LOG.info("highPrecisionUnicodeAthensAcropolis2");
73+
final Point point = MapcodeCodec.decode("GRC \u0397\u03a0.\u03982-\u03a62");
74+
assertEquals("decodeUnicode latitude", 37971844, point.getLatMicroDeg());
75+
assertEquals("decodeUnicode longitude", 23726223,
76+
point.getLonMicroDeg());
7777
}
7878

7979
@Test
8080
public void unicodeMapcodeAthensAcropolis1() throws Exception {
81-
LOG.info("unicodeMapcodeAthensAcropolis1");
82-
final Point point = MapcodeCodec.decode("ΗΠ.Θ2", Territory.GRC);
83-
assertEquals("decodeUnicode latitude", 37971812, point.getLatMicroDeg());
84-
assertEquals("decodeUnicode longitude", 23726247,
85-
point.getLonMicroDeg());
81+
LOG.info("unicodeMapcodeAthensAcropolis1");
82+
final Point point = MapcodeCodec.decode("\u0397\u03a0.\u03982", Territory.GRC);
83+
assertEquals("decodeUnicode latitude", 37971812, point.getLatMicroDeg());
84+
assertEquals("decodeUnicode longitude", 23726247,
85+
point.getLonMicroDeg());
8686
}
8787

8888
@Test
8989
public void unicodeMapcodeAthensAcropolis2() throws Exception {
90-
LOG.info("unicodeMapcodeAthensAcropolis2");
91-
final Point point = MapcodeCodec.decode("GRC ΗΠ.Θ2");
92-
assertEquals("decodeUnicode latitude", 37971812, point.getLatMicroDeg());
93-
assertEquals("decodeUnicode longitude", 23726247,
94-
point.getLonMicroDeg());
90+
LOG.info("unicodeMapcodeAthensAcropolis2");
91+
final Point point = MapcodeCodec.decode("GRC \u0397\u03a0.\u03982");
92+
assertEquals("decodeUnicode latitude", 37971812, point.getLatMicroDeg());
93+
assertEquals("decodeUnicode longitude", 23726247,
94+
point.getLonMicroDeg());
9595
}
9696

9797
@Test
9898
public void unicodeMapcodeTokyoTower1() throws Exception {
99-
LOG.info("unicodeMapcodeTokyoTower1");
100-
final Point point = MapcodeCodec.decode("\u30c1\u30ca.8\u30c1",
101-
Territory.JPN);
102-
assertEquals("decodeUnicode latitude", 35658660, point.getLatMicroDeg());
103-
assertEquals("decodeUnicode longitude", 139745394,
104-
point.getLonMicroDeg());
99+
LOG.info("unicodeMapcodeTokyoTower1");
100+
final Point point = MapcodeCodec.decode("\u30c1\u30ca.8\u30c1",
101+
Territory.JPN);
102+
assertEquals("decodeUnicode latitude", 35658660, point.getLatMicroDeg());
103+
assertEquals("decodeUnicode longitude", 139745394,
104+
point.getLonMicroDeg());
105105
}
106106

107107
@Test
108108
public void unicodeMapcodeTokyoTower2() throws Exception {
109-
LOG.info("unicodeMapcodeTokyoTower2");
110-
final Point point = MapcodeCodec.decode("JPN \u30c1\u30ca.8\u30c1");
111-
assertEquals("decodeUnicode latitude", 35658660, point.getLatMicroDeg());
112-
assertEquals("decodeUnicode longitude", 139745394,
113-
point.getLonMicroDeg());
109+
LOG.info("unicodeMapcodeTokyoTower2");
110+
final Point point = MapcodeCodec.decode("JPN \u30c1\u30ca.8\u30c1");
111+
assertEquals("decodeUnicode latitude", 35658660, point.getLatMicroDeg());
112+
assertEquals("decodeUnicode longitude", 139745394,
113+
point.getLonMicroDeg());
114114
}
115115

116116
@Test
117117
public void mapCodeWithZeroGroitzsch() throws Exception {
118-
LOG.info("mapCodeWithZeroGroitzsch");
119-
final Point point = MapcodeCodec.decode("HMVM.3Q0", Territory.DEU);
120-
assertEquals("decodeUnicode latitude", 51154852, point.getLatMicroDeg());
121-
assertEquals("decodeUnicode longitude", 12278574,
122-
point.getLonMicroDeg());
118+
LOG.info("mapCodeWithZeroGroitzsch");
119+
final Point point = MapcodeCodec.decode("HMVM.3Q0", Territory.DEU);
120+
assertEquals("decodeUnicode latitude", 51154852, point.getLatMicroDeg());
121+
assertEquals("decodeUnicode longitude", 12278574,
122+
point.getLonMicroDeg());
123123
}
124124

125-
@Test(expected = UnknownMapcodeException.class)
125+
@Test(expected = IllegalArgumentException.class)
126126
public void invalidTerritory() throws Exception {
127127
LOG.info("invalidTerritory");
128128
MapcodeCodec.decode("NLD 49.4V", Territory.NLD);
129129
}
130130

131-
@Test(expected = UnknownMapcodeException.class)
131+
@Test(expected = IllegalArgumentException.class)
132132
public void invalidNoDot() throws Exception {
133133
LOG.info("invalidNoDot");
134134
MapcodeCodec.decode("494V", Territory.NLD);
135135
}
136136

137-
@Test(expected = UnknownMapcodeException.class)
137+
@Test(expected = IllegalArgumentException.class)
138138
public void invalidDotLocation1() throws Exception {
139139
LOG.info("invalidDotLocation1");
140140
MapcodeCodec.decode("4.94V", Territory.NLD);
141141
}
142142

143-
@Test(expected = UnknownMapcodeException.class)
143+
@Test(expected = IllegalArgumentException.class)
144144
public void invalidDotLocation2() throws Exception {
145145
LOG.info("invalidDotLocation2");
146146
MapcodeCodec.decode("494.V", Territory.NLD);
147147
}
148148

149-
@Test(expected = UnknownMapcodeException.class)
149+
@Test(expected = IllegalArgumentException.class)
150150
public void invalidDotLocation3() throws Exception {
151151
LOG.info("invalidDotLocation3");
152152
MapcodeCodec.decode("494V49.4V", Territory.NLD);
153153
}
154154

155155
@Test(expected = UnknownMapcodeException.class)
156-
public void invalidDotLocation4() throws Exception {
157-
LOG.info("invalidDotLocation4");
156+
public void invalidMapcode1() throws Exception {
157+
LOG.info("invalidMapcode1");
158158
MapcodeCodec.decode("494.V494V", Territory.NLD);
159159
}
160160

161-
@Test(expected = UnknownMapcodeException.class)
161+
@Test(expected = IllegalArgumentException.class)
162162
public void invalidHighPrecisionCharacter() throws Exception {
163-
LOG.info("invalidHighPrecisionCharacter");
164-
MapcodeCodec.decode("49.4V-Z", Territory.NLD);
163+
LOG.info("invalidHighPrecisionCharacter");
164+
MapcodeCodec.decode("49.4V-Z", Territory.NLD);
165165
}
166166

167-
@Test(expected = UnknownMapcodeException.class)
167+
@Test(expected = IllegalArgumentException.class)
168168
public void invalidHighPrecisionCharacter2() throws Exception {
169-
LOG.info("invalidHighPrecisionCharacter2");
170-
MapcodeCodec.decode("49.4V-HZ", Territory.NLD);
169+
LOG.info("invalidHighPrecisionCharacter2");
170+
MapcodeCodec.decode("49.4V-HZ", Territory.NLD);
171171
}
172172

173-
@Test(expected = UnknownMapcodeException.class)
173+
@Test(expected = IllegalArgumentException.class)
174174
public void invalidHighPrecisionCharacter3() throws Exception {
175-
LOG.info("invalidHighPrecisionCharacter3");
176-
MapcodeCodec.decode("ΗΠ.Θ2-Б", Territory.GRC);
175+
LOG.info("invalidHighPrecisionCharacter3");
176+
MapcodeCodec.decode("\u0397\u03a0.\u03982-\u0411", Territory.GRC);
177177
}
178178

179-
@Test(expected = UnknownMapcodeException.class)
179+
@Test(expected = IllegalArgumentException.class)
180180
public void invalidHighPrecisionCharacter4() throws Exception {
181-
LOG.info("invalidHighPrecisionCharacter4");
182-
MapcodeCodec.decode("ΗΠ.Θ2-ББ", Territory.GRC);
181+
LOG.info("invalidHighPrecisionCharacter4");
182+
MapcodeCodec.decode("\u0397\u03a0.\u03982-\u0411\u0411", Territory.GRC);
183183
}
184184

185185
@SuppressWarnings("ConstantConditions")

0 commit comments

Comments
 (0)