/* Copyright (c) 2011 Danish Maritime Authority. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dk.dma.ais.sentence; import dk.dma.ais.binary.SixbitException; import dk.dma.ais.proprietary.IProprietarySourceTag; import dk.dma.ais.proprietary.IProprietaryTag; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; import java.util.List; /** * Abstract base class for representing IEC sentences */ public abstract class Sentence { protected String delimiter = "!"; protected String talker; protected String formatter; protected int checksum; protected String msgChecksum; protected String sentenceStr; protected List<String> orgLines = new ArrayList<>(); protected List<String> rawSentences = new ArrayList<>(); protected LinkedList<String> encodedFields; protected CommentBlock commentBlock; protected LinkedList<IProprietaryTag> tags; // Possible proprietary source tags for the message protected Date mssisTimestamp; /** * Abstract method that all sentence classes must implement * * The method handles assembly and extraction of the 6-bit data from sentences. The sentence is expected to be in order. * * It will return an error if received line is out of order or from a new sequence before the previous one is finished. * * @param line * @return 0 Complete packet - 1 Incomplete packet */ public abstract int parse(SentenceLine sl) throws SentenceException, SixbitException; /** * Get an encoded sentence * * @return */ public abstract String getEncoded(); /** * Basic parse of line into sentence parts * * @param line * @throws SentenceException */ protected void baseParse(SentenceLine sl) throws SentenceException { this.orgLines.add(sl.getLine()); // Save raw sentence rawSentences.add(sl.getSentence()); // Check for comment block if (sl.getPrefix().length() > 0 && CommentBlock.hasCommentBlock(sl.getPrefix())) { addCommentBlock(sl.getPrefix()); } // Check checksum if (!sl.isChecksumMatch()) { throw new SentenceException("Invalid checksum for line: " + sl.getLine() + ": " + sl.getChecksumField() + " should have been: " + sl.getChecksumString()); } if (sl.getFields().size() < 2) { throw new SentenceException("Invalid sentence, less than two fields"); } // Check talker/formatter if (sl.getTalker() == null || sl.getFormatter() == null) { throw new SentenceException("Invalid sentence, wrong talker/formatter: " + sl.getFields().get(0)); } // Try to get MSSIS timestamp mssisTimestamp = findMssisTimestamp(sl); } public void addSingleCommentBlock(String line) throws SentenceException { this.orgLines.add(line); addCommentBlock(line); } private void addCommentBlock(String line) throws SentenceException { if (commentBlock == null) { commentBlock = new CommentBlock(); } try { commentBlock.addLine(line); } catch (CommentBlockException e) { throw new SentenceException("CommentBlockException: " + e.getMessage()); } } /** * The top most encode method */ protected void encode() { encodedFields = new LinkedList<>(); encodedFields.add(delimiter + talker + formatter); } /** * Method to finalize the encoding and return sentence * * @return final sentence */ protected String finalEncode() { // Join fields String encoded = StringUtils.join(encodedFields.iterator(), ','); this.sentenceStr = encoded; try { calculateChecksum(); } catch (SentenceException e) { e.printStackTrace(); } msgChecksum = getStringChecksum(checksum); encoded += "*" + msgChecksum; return encoded; } /** * Calculate checksum of this sentence * * @throws SentenceException */ private void calculateChecksum() throws SentenceException { this.checksum = getChecksum(sentenceStr); } /** * Calculate checksum of sentence * * @param sentence * @return * @throws SentenceException */ public static int getChecksum(String sentence) throws SentenceException { int checksum = 0; for (int i = 1; i < sentence.length(); i++) { char c = sentence.charAt(i); if (c == '!' || c == '$') { throw new SentenceException("Start Character Found before Checksum"); } if (c == '*') { break; } checksum ^= c; } return checksum; } /** * Get checksum string representation * * @param checksum * @return */ public static String getStringChecksum(int checksum) { String strChecksum = Integer.toString(checksum, 16).toUpperCase(); if (strChecksum.length() < 2) { strChecksum = "0" + strChecksum; } return strChecksum; } /** * Try to get the proprietary MSSIS timestamp appended to the NMEA sentence * * @return */ public static Date findMssisTimestamp(SentenceLine sentenceLine) { if (sentenceLine == null) { return null; } // Go through postfix fields int start = sentenceLine.getPostfixStart(); if (start < 0) { return null; } for (int i = start; i < sentenceLine.getFields().size(); i++) { String field = sentenceLine.getFields().get(i); try { return new Date(Long.parseLong(field) * 1000); } catch (NumberFormatException e) { } } return null; } /** * Get found MSSIS timestamp * @return */ public Date getMssisTimestamp() { return mssisTimestamp; } /** * Try to get timestamp in this order: Comment block timestamp, proprietary tag timestamp and MSSIS timestamp * * @return */ public Date getTimestamp() { // Try comment block first CommentBlock cb = getCommentBlock(); if (cb != null) { Long ts = cb.getTimestamp(); if (ts != null) { return new Date(ts * 1000); } } // Try from proprietary source tags if (getTags() != null) { for (IProprietaryTag tag : getTags()) { if (tag instanceof IProprietarySourceTag) { Date t = ((IProprietarySourceTag) tag).getTimestamp(); if (t != null) { return t; } } } } // Return MSSIS timestamp return mssisTimestamp; } public static int parseInt(String str) throws SentenceException { if (str == null || str.length() == 0) { throw new SentenceException("Invalid integer field: " + str); } try { return Integer.parseInt(str); } catch (NumberFormatException e) { throw new SentenceException("Invalid integer field: " + str); } } /** * Determine if a line seems to contain a sentence There should be a ! or $ * * @param line * @return */ public static boolean hasSentence(String line) { return line.indexOf('!') >= 0 || line.indexOf('$') >= 0; } /** * Get original lines for this sentence * * @return */ public List<String> getOrgLines() { return orgLines; } /** * Get original lines joined by carriage return line feed * * @return */ public String getOrgLinesJoined() { return StringUtils.join(orgLines.iterator(), "\r\n"); } /** * Get original raw sentences without any prefix * * @return */ public List<String> getRawSentences() { return rawSentences; } /** * Get original raw sentences without any prefix joined by carriage return line feed * * @return */ public String getRawSentencesJoined() { return StringUtils.join(rawSentences.iterator(), "\r\n"); } /** * Set talker * * @param talker */ public void setTalker(String talker) { this.talker = talker; } /** * Set delimiter * * @param delimiter */ public void setDelimiter(String delimiter) { this.delimiter = delimiter; } /** * Set formatter * * @param formatter */ public void setFormatter(String formatter) { this.formatter = formatter; } /** * Get possible comment block * * @return */ public CommentBlock getCommentBlock() { return commentBlock; } /** * Get all tags * * @return */ public LinkedList<IProprietaryTag> getTags() { return tags; } public void setTags(LinkedList<IProprietaryTag> tags) { this.tags = tags; } /** * Return the LAST source tag (closest to AIS sentence) * * @return */ public IProprietarySourceTag getSourceTag() { if (tags == null) { return null; } // Iterate backwards for (Iterator<IProprietaryTag> iterator = tags.descendingIterator(); iterator.hasNext();) { IProprietaryTag tag = iterator.next(); if (tag instanceof IProprietarySourceTag) { return (IProprietarySourceTag) tag; } } return null; } /** * Add tag (to front) * * @param sourceTag */ public void setTag(IProprietaryTag tag) { if (this.tags == null) { this.tags = new LinkedList<>(); } this.tags.addFirst(tag); } /** * Convert any sentence to new sentence with !<talker><formatter>,...., * * @param sentence * @param talker * @param formatter * @return * @throws SentenceException */ public static String convert(String sentence, String talker, String formatter) throws SentenceException { String newSentence = sentence.trim(); newSentence = newSentence.substring(6, newSentence.length() - 3); newSentence = "!" + talker + formatter + newSentence; String checksum = Integer.toString(getChecksum(newSentence), 16).toUpperCase(); if (checksum.length() < 2) { checksum = "0" + checksum; } newSentence += "*" + checksum; return newSentence; } }