View Javadoc
1   package au.gov.amsa.ais;
2   
3   import java.nio.charset.Charset;
4   import java.util.Arrays;
5   import java.util.TimeZone;
6   
7   import com.google.common.annotations.VisibleForTesting;
8   
9   /**
10   * Ais utility methods.
11   */
12  public final class Util {
13  
14  	private Util() {
15  		// for test coverage
16  	}
17  
18  	static void forTestCoverageOnly() {
19  		new Util();
20  	}
21  
22  	/**
23  	 * Get a value for specified bits from the binary string.
24  	 * 
25  	 * @param fromBit
26  	 * @param toBit
27  	 * @return
28  	 * @throws AisParseException
29  	 */
30  	protected static int getValueByBinStr(String binaryString, boolean signed) {
31  
32  		Integer value = Integer.parseInt(binaryString, 2);
33  		if (signed && binaryString.charAt(0) == '1') {
34  			char[] invert = new char[binaryString.length()];
35  			Arrays.fill(invert, '1');
36  			value ^= Integer.parseInt(new String(invert), 2);
37  			value += 1;
38  			value = -value;
39  		}
40  
41  		return value;
42  	}
43  
44  	static TimeZone TIME_ZONE_UTC = TimeZone.getTimeZone("UTC");
45  
46  	private static Charset ASCII_8_BIT_CHARSET = Charset.forName("ISO-8859-1");
47  
48  	/**
49  	 * Returns decoded message from ascii 8 bit to 6 bit binary then to
50  	 * characters.
51  	 * 
52  	 * @param encodedMessage
53  	 * @return
54  	 */
55  	protected static String decodeMessage(String encodedMessage) {
56  		return getDecodedStr(ascii8To6bitBin(encodedMessage.getBytes(ASCII_8_BIT_CHARSET)));
57  	}
58  
59  	/**
60  	 * Returns conversion of ASCII-coded character to 6-bit binary byte array.
61  	 * 
62  	 * @param toDecBytes
63  	 * @return decodedBytes
64  	 */
65  	@VisibleForTesting
66  	static byte[] ascii8To6bitBin(byte[] toDecBytes) {
67  
68  		byte[] convertedBytes = new byte[toDecBytes.length];
69  		int sum = 0;
70  		int _6bitBin = 0;
71  
72  		for (int i = 0; i < toDecBytes.length; i++) {
73  			sum = 0;
74  			_6bitBin = 0;
75  
76  			if (toDecBytes[i] < 48) {
77  				throw new AisParseException(AisParseException.INVALID_CHARACTER + " "
78  				        + (char) toDecBytes[i]);
79  			} else {
80  				if (toDecBytes[i] > 119) {
81  					throw new AisParseException(AisParseException.INVALID_CHARACTER + " "
82  					        + (char) toDecBytes[i]);
83  				} else {
84  					if (toDecBytes[i] > 87) {
85  						if (toDecBytes[i] < 96) {
86  							throw new AisParseException(AisParseException.INVALID_CHARACTER + " "
87  							        + (char) toDecBytes[i]);
88  						} else {
89  							sum = toDecBytes[i] + 40;
90  						}
91  					} else {
92  						sum = toDecBytes[i] + 40;
93  					}
94  					if (sum != 0) {
95  						if (sum > 128) {
96  							sum += 32;
97  						} else {
98  							sum += 40;
99  						}
100 						_6bitBin = sum & 0x3F;
101 						convertedBytes[i] = (byte) _6bitBin;
102 					}
103 				}
104 			}
105 		}
106 
107 		return convertedBytes;
108 	}
109 
110 	/**
111 	 * Get decoded string from bytes.
112 	 * 
113 	 * @param decBytes
114 	 */
115 	private static String getDecodedStr(byte[] decBytes) {
116 
117 		// prepare StringBuilder with capacity being the smallest power of 2
118 		// greater than decBytes.length*6
119 		int n = decBytes.length * 6;
120 		int capacity = leastPowerOf2GreaterThanOrEqualTo(n);
121 		StringBuilder decStr = new StringBuilder(capacity);
122 
123 		for (int i = 0; i < decBytes.length; i++) {
124 
125 			int decByte = decBytes[i];
126 			String bitStr = Integer.toBinaryString(decByte);
127 
128 			int padding = Math.max(0, 6 - bitStr.length());
129 
130 			for (int j = 0; j < padding; j++) {
131 				decStr.append('0');
132 			}
133 
134 			for (int j = 0; j < 6 - padding; j++) {
135 				decStr.append(bitStr.charAt(j));
136 			}
137 		}
138 		return decStr.toString();
139 	}
140 
141 	static int leastPowerOf2GreaterThanOrEqualTo(int n) {
142 		return n > 1 ? Integer.highestOneBit(n - 1) << 1 : 1;
143 	}
144 
145 	/**
146 	 * Decode 6 bit String to standard ASCII String
147 	 * 
148 	 * Input is a binary string of 0 and 1 each 6 bit is a character that will
149 	 * be converted to the standard ASCII character
150 	 * 
151 	 * @param str
152 	 * @return
153 	 */
154 	protected static String getAsciiStringFrom6BitStr(String str) {
155 
156 		StringBuilder txt = new StringBuilder();
157 		for (int i = 0; i < str.length(); i = i + 6) {
158 			byte _byte = (byte) Integer.parseInt(str.substring(i, i + 6), 2);
159 			_byte = convert6BitCharToStandardAscii(_byte);
160 			char convChar = (char) _byte;
161 
162 			if (convChar == '@') {
163 				break;
164 			}
165 			txt.append((char) _byte);
166 		}
167 
168 		return txt.toString().trim();
169 	}
170 
171 	/**
172 	 * Convert one 6 bit ASCII character to 8 bit ASCII character
173 	 * 
174 	 * @param byteToConvert
175 	 * @return
176 	 */
177 	@VisibleForTesting
178 	static byte convert6BitCharToStandardAscii(byte byteToConvert) {
179 
180 		byte b = 0;
181 		if (byteToConvert < 32) {
182 			b = (byte) (byteToConvert + 64);
183 		} else if (byteToConvert < 63) {
184 			b = byteToConvert;
185 		}
186 
187 		return b;
188 	}
189 
190 	/**
191 	 * Check lat lon are withing allowable range as per 1371-4.pdf. Note that
192 	 * values of long=181, lat=91 have special meaning.
193 	 * 
194 	 * @param lat
195 	 * @param lon
196 	 */
197 	public static void checkLatLong(double lat, double lon) {
198 		checkArgument(lon <= 181.0, "longitude out of range " + lon);
199 		checkArgument(lon > -180.0, "longitude out of range " + lon);
200 		checkArgument(lat <= 91.0, "latitude out of range " + lat);
201 		checkArgument(lat > -90.0, "latitude out of range " + lat);
202 	}
203 
204 	/**
205 	 * Check lat is within allowable range as per 1371-4.pdf. Note that value of
206 	 * lat=91 has special meaning.
207 	 * 
208 	 * @param lat
209 	 */
210 	public static void checkLat(double lat) {
211 		checkArgument(lat <= 91.0, "latitude out of range ");
212 		checkArgument(lat > -90.0, "latitude out of range ");
213 	}
214 
215 	/**
216 	 * Check lon is within allowable range as per 1371-4.pdf. Note that value of
217 	 * long=181, has special meaning.
218 	 * 
219 	 * @param lon
220 	 */
221 	public static void checkLong(double lon) {
222 		checkArgument(lon <= 181.0, "longitude out of range");
223 		checkArgument(lon > -180.0, "longitude out of range");
224 	}
225 
226 	/**
227 	 * Throws an AisParseException with given message if b is false.
228 	 * 
229 	 * @param b
230 	 * @param message
231 	 */
232 	public static void checkArgument(boolean b, String message) {
233 		if (!b)
234 			throw new AisParseException(message);
235 	}
236 
237 	private static AisExtractorFactory extractorFactory = new AisExtractorFactory() {
238 
239 		@Override
240 		public AisExtractor create(String message, int minLength, int padBits) {
241 			return new AisExtractor(message, minLength, padBits);
242 		}
243 
244 	};
245 
246 	/**
247 	 * Returns singleton {@link AisExtractorFactory}.
248 	 * 
249 	 * @return
250 	 */
251 	public static AisExtractorFactory getAisExtractorFactory() {
252 		return extractorFactory;
253 	}
254 
255 	/**
256 	 * Check message id corresponds to one of the given list of message types.
257 	 * 
258 	 * @param messageId
259 	 * @param messageTypes
260 	 */
261 	public static void checkMessageId(int messageId, AisMessageType... messageTypes) {
262 		boolean found = false;
263 		for (AisMessageType messageType : messageTypes) {
264 			if (messageType.getId() == messageId)
265 				found = true;
266 		}
267 		if (!found) {
268 			StringBuffer s = new StringBuffer();
269 			for (AisMessageType messageType : messageTypes) {
270 				if (s.length() > 0)
271 					s.append(",");
272 				s.append(messageType.getId() + "");
273 			}
274 			checkArgument(found, "messageId must be in [" + s + "]  but was " + messageId);
275 		}
276 	}
277 
278 	/**
279 	 * Returns true if and only if given integers are equal. This was extracted
280 	 * to assist in source code branch coverage.
281 	 * 
282 	 * @param i
283 	 * @param j
284 	 * @return
285 	 */
286 	public static boolean areEqual(int i, int j) {
287 		return i == j;
288 	}
289 
290 	/**
291 	 * Returns true if and only if given messageId corresponds to a class A
292 	 * position report (message ids 1,2,3).
293 	 * 
294 	 * @param messageId
295 	 * @return
296 	 */
297 	public static boolean isClassAPositionReport(int messageId) {
298 		return messageId == 1 || messageId == 2 || messageId == 3;
299 	}
300 
301 }