/* 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.packet; import com.google.common.collect.AbstractIterator; import dk.dma.ais.message.AisMessage; import dk.dma.ais.sentence.Abk; import dk.dma.ais.sentence.SentenceException; import dk.dma.ais.sentence.SentenceLine; import dk.dma.commons.util.io.CountingInputStream; import dk.dma.commons.util.io.OutputStreamSink; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Iterator; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import static java.util.Objects.requireNonNull; /** * Class for reading AIS packet streams. * * @author Kasper Nielsen */ public class AisPacketReader implements AutoCloseable, Iterable<AisPacket> { /** The logger */ static final Logger LOG = LoggerFactory.getLogger(AisPacketReader.class); /** The number of bytes read by this instance. */ private final AtomicLong bytesRead = new AtomicLong(); /** Whether or not this reader has been closed. */ volatile boolean closed; /** The number of lines read by this instance. */ private final AtomicLong linesRead = new AtomicLong(); /** Reader to parse lines and deliver complete AIS packets. */ protected final AisPacketParser packetReader = new AisPacketParser(); /** The number of packets read by this instance. */ private final AtomicLong packetsRead = new AtomicLong(); /** The wrapped reader. */ final BufferedReader reader; /** The wrapped input stream. */ final InputStream stream; /** * Sometimes we expect input to only contain valid messages. So we want to throw an exception in case we meet some * unknown content. This was added when trying to find a bug, where we where trying to read a gzipped stream. */ final boolean throwExceptions; /** * Create * * @param stream * the input stream to read data from */ public AisPacketReader(InputStream stream) { this(stream, false); } AisPacketReader(InputStream stream, boolean errorFree) { this.stream = requireNonNull(stream); this.reader = new BufferedReader(new InputStreamReader(new CountingInputStream(stream, bytesRead), StandardCharsets.US_ASCII)); this.throwExceptions = errorFree; } public void close() throws IOException { stream.close(); closed = true; } /** * Returns the number of bytes read by this reader. * * @return the number of bytes read by this reader */ public long getNumberOfBytesRead() { return bytesRead.get(); } /** * Returns the number of lines read by this reader. * * @return the number of lines read by this reader */ public long getNumberOfLinesRead() { return linesRead.get(); } /** * Returns the number of packets read by this reader. * * @return the number of packets read by this reader */ public long getNumberOfPacketsRead() { return packetsRead.get(); } /** * Override this method to handle {@link Abk} sentences. * * @param abk * the sentence to handle */ protected void handleAbk(Abk abk) {} /** * Handle a received line * * @param line * @return */ private AisPacket handleLine(String line) throws IOException { linesRead.incrementAndGet(); // Check for ABK if (Abk.isAbk(line)) { if (LOG.isDebugEnabled()) { LOG.debug("Received ABK: " + line); } Abk abk = new Abk(); try { abk.parse(new SentenceLine(line)); handleAbk(abk); } catch (Exception e) { if (throwExceptions) { throw new IOException(e); } LOG.error("Failed to parse ABK: " + line + ": " + e.getMessage()); } packetReader.newVdm(); return null; } try { return packetReader.readLine(line); } catch (SentenceException se) { if (throwExceptions) { throw new IOException(se); } LOG.error("Sentence error: " + line + " (possible related proptag: " + se.getPossibleProprietaryTag() + ")"); LOG.debug("Sentence trace: " + se.getMessage()); return null; } catch (Exception e) { if (throwExceptions) { throw new IOException(e); } LOG.error("Sentence line error: " + line); LOG.debug("Sentence line error: " + e.getMessage()); return null; } } @SafeVarargs public final void forEachRemaining(Consumer<? super AisPacket>... consumers) throws IOException { requireNonNull(consumers); AisPacket p; while ((p = readPacket()) != null) { for (Consumer<? super AisPacket> c : consumers) { c.accept(p); } } } @SafeVarargs public final void forEachRemainingMessage(Consumer<? super AisMessage>... consumers) throws IOException { requireNonNull(consumers); AisPacket p; while ((p = readPacket()) != null) { AisMessage m = p.tryGetAisMessage(); if (m != null) { for (Consumer<? super AisMessage> c : consumers) { c.accept(m); } } } } /** * @return the next packet or null if the end of the stream has been reached * @throws IOException * if an exception occurred while reading the packet */ public AisPacket readPacket() throws IOException { return readPacket0(); } /** * Reads the next AisPacket */ AisPacket readPacket0() throws IOException { for (String line = reader.readLine(); line != null; line = reader.readLine()) { if (closed) { return null; } AisPacket p = handleLine(line); if (p != null) { packetsRead.incrementAndGet(); return p; } } return null; } /** * Returns a AIS packet stream running in a new thread. * * @return a AIS packet stream running in a new thread */ public AisPacketStream stream() { return stream(Executors.newSingleThreadExecutor()); } /** * Returns a AIS packet stream using the specified executor. * * @return a AIS packet stream using the specified executor */ public AisPacketStream stream(Executor e) { final AisPacketStream s = AisPacketStream.newStream(); e.execute(new Runnable() { public void run() { try { for (AisPacket p = readPacket(); p != null; p = readPacket()) { s.add(p); } } catch (IOException e) { LOG.error("Failed to read packet: ", e); } } }); return s.immutableStream(); } public static AisPacketReader createFromSystemResource(String resourceName, boolean throwExceptions) throws IOException { URL url = ClassLoader.getSystemResource(resourceName); if (url == null) { throw new IllegalArgumentException("Could not find system resource: " + resourceName); } InputStream inputStream = url.openStream(); return new AisPacketReader(inputStream, throwExceptions); } /** * Creates a new AIS packet reader from the specified file. If the specified file has a '.zip' suffix. The file is * automatically treated as a zip file. * * @param p * the path of the file * @param throwExceptions * whether to throw exceptions or just log them * @return a new reader * @throws IOException * if the reader failed to be constructed, for example, if the specified file does not exist */ public static AisPacketReader createFromFile(Path p, boolean throwExceptions) throws IOException { InputStream is = Files.newInputStream(p); BufferedInputStream bis = new BufferedInputStream(is); if (!p.getFileName().toString().endsWith(".zip")) { return new AisPacketReader(bis, throwExceptions); // not a zip file } final ZipInputStream zis = new ZipInputStream(bis); return new AisPacketReader(zis, throwExceptions) { ZipEntry e; boolean isFirst = true; /** {@inheritDoc} */ @Override AisPacket readPacket0() throws IOException { if (!isFirst && e == null) { return null; // already empty } else if (isFirst) { e = zis.getNextEntry(); isFirst = false; } AisPacket p = super.readPacket0(); // while the packet is null and we still have more zip entries while (p == null && e != null) { e = zis.getNextEntry(); p = super.readPacket0(); } return p; } }; } /** * Writes the reminder of packets to the output stream using the specified sink * * @param os * the output stream to write to * @param sink * the sink to write to * @throws IOException * if the a packet could not be written */ public void writeTo(OutputStream os, OutputStreamSink<AisPacket> sink) throws IOException { sink.header(os); long count = 0; AisPacket p = readPacket(); while (p != null) { count++; sink.process(os, p, count); p = readPacket(); } sink.footer(os, count); } /** {@inheritDoc} */ @Override public Iterator<AisPacket> iterator() { return new IteratorImpl(); } class IteratorImpl extends AbstractIterator<AisPacket> { /** {@inheritDoc} */ @Override protected AisPacket computeNext() { try { AisPacket p = readPacket(); if (p == null) { super.endOfData(); } return p; } catch (IOException e) { throw new RuntimeException(e); } } } }