/* 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.transform;
import dk.dma.ais.binary.SixbitException;
import dk.dma.ais.message.AisBinaryMessage;
import dk.dma.ais.message.AisMessage;
import dk.dma.ais.message.AisMessage5;
import dk.dma.ais.message.AisStaticCommon;
import dk.dma.ais.message.binary.AisApplicationMessage;
import dk.dma.ais.packet.AisPacket;
import dk.dma.ais.proprietary.IProprietaryTag;
import dk.dma.ais.sentence.CommentBlock;
import dk.dma.ais.sentence.Vdm;
import dk.dma.enav.model.Country;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
/**
* Transformer that anonymizes ais packets
*/
@ThreadSafe
public class AnonymousTransformer implements IAisPacketTransformer {
private static final Logger LOG = LoggerFactory.getLogger(AnonymousTransformer.class);
/**
* List of available MID's
*/
private static final ArrayList<Integer> MID_LIST = new ArrayList<>(Country.getMidMap().keySet());
/**
* List of random names
*/
private static final ArrayList<String> NAME_LIST = new ArrayList<>();
/**
* Location of random names file
*/
private static final String LOCATION = AnonymousTransformer.class.getPackage().getName().replace(".", "/")
+ "/names.txt";
/**
* Load names and make random order
*/
static {
URL url = ClassLoader.getSystemResource(LOCATION);
if (url == null) {
url = Thread.currentThread().getContextClassLoader().getResource(LOCATION);
}
if (url == null) {
throw new Error("Could not locate " + LOCATION + " on classpath");
}
try {
InputStreamReader in = new InputStreamReader(url.openStream(), StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(in);
String line;
Set<String> names = new HashSet<>();
while ((line = reader.readLine()) != null) {
names.add(line.trim().toUpperCase());
}
NAME_LIST.addAll(names);
Collections.shuffle(NAME_LIST);
} catch (IOException e) {
throw new Error("Failed to load random names list: " + e.getMessage());
}
}
/**
* Map from MMSI to anonymized data
*/
@GuardedBy("this")
private final Map<Integer, AnonData> anonDataMap = new HashMap<>();
/**
* Counter to keep track of MMSI to anonymous MMSI map
*/
@GuardedBy("this")
private int counter;
/**
* Random number generator
*/
private final Random rand;
public AnonymousTransformer() {
rand = new Random(System.currentTimeMillis());
}
@Override
public AisPacket transform(AisPacket packet) {
// Get AIS message or discard
AisMessage message = packet.tryGetAisMessage();
if (message == null) {
return null;
}
// Get anonymized data for target
AnonData anonData = getAnonData(message);
// Transformation for all mesasges
anonymize(message, anonData);
// Transformation for general static data
if (message instanceof AisStaticCommon) {
anonymize((AisStaticCommon) message, anonData);
}
// Transformation for class A static data
if (message instanceof AisMessage5) {
anonymize((AisMessage5) message, anonData);
}
return createPacket(message, packet);
}
private synchronized AnonData getAnonData(AisMessage message) {
// Try to get from map
AnonData anonData = anonDataMap.get(message.getUserId());
if (anonData != null) {
return anonData;
}
// Make anonymized data
String name = makeName(counter);
int mmsi = makeMmsi(counter++);
String callsign = makeCallsign();
String destination = makeDestination();
int imoNo = counter;
anonData = new AnonData(mmsi, name, imoNo, callsign, destination);
anonDataMap.put(message.getUserId(), anonData);
return anonData;
}
/**
* Make MMSI from random country and counter
*
* @param id
* @return
*/
private int makeMmsi(int id) {
return MID_LIST.get(rand.nextInt(MID_LIST.size())) * 1000000 + id;
}
/**
* Get random name from list of names
*
* @return
*/
private String makeName(int id) {
return NAME_LIST.get(id % NAME_LIST.size());
}
/**
* Make random callsign
*
* @return
*/
private String makeCallsign() {
return UUID.randomUUID().toString().substring(0, 5).toUpperCase();
}
/**
* Make random destination
*/
private String makeDestination() {
return "N/A";
}
/**
* Do transformation basic for all AIS messages
*
* @param message
*/
private void anonymize(AisMessage message, AnonData anonData) {
message.setUserId(anonData.getMmsi());
}
/**
*
* @param posMessage
*/
private void anonymize(AisStaticCommon message, AnonData anonData) {
// Change name
message.setName(anonData.getName());
// Callsign
message.setCallsign(anonData.getCallsign());
}
private void anonymize(AisMessage5 message, AnonData anonData) {
// IMO
message.setImo(anonData.getImoNo());
// Dest
message.setDest(anonData.getDestination());
}
/**
* Make a new packet from old packet and
*
* @param message
* @param packet
* @return
*/
private AisPacket createPacket(AisMessage message, AisPacket packet) {
List<String> lines = new ArrayList<>(4);
int sequence = message.getVdm().getSequence();
// Handle binary message
if (message instanceof AisBinaryMessage) {
try {
AisApplicationMessage appMessage = ((AisBinaryMessage) message).getApplicationMessage();
if (appMessage == null) {
return null;
}
} catch (SixbitException e) {
return null;
}
}
String[] sentences;
try {
sentences = Vdm.createSentences(message, sequence);
} catch (UnsupportedOperationException e) {
LOG.debug("Encoding not supported by message type " + message.getMsgId());
return null;
} catch (SixbitException e) {
LOG.error("Failed to encode AIS message: " + message + ": " + e.getMessage());
return null;
}
// Add proprietary tags (or not?)
if (message.getTags() != null) {
for (IProprietaryTag tag : message.getTags()) {
lines.add(tag.getSentence());
}
}
// Add comment blocks
CommentBlock cb = message.getVdm().getCommentBlock();
if (cb != null) {
lines.add(cb.encode());
}
// Add sentences
for (String sentence : sentences) {
lines.add(sentence);
}
return AisPacket.from(StringUtils.join(lines, "\r\n"));
}
/**
* Class holding anonymized data for target
*/
private class AnonData {
private final int mmsi;
private final String name;
private final int imoNo;
private final String callsign;
private final String destination;
public AnonData(int mmsi, String name, int imoNo, String callsign, String destination) {
this.mmsi = mmsi;
this.name = name;
this.imoNo = imoNo;
this.callsign = callsign;
this.destination = destination;
}
public int getMmsi() {
return mmsi;
}
public String getName() {
return name;
}
public int getImoNo() {
return imoNo;
}
public String getCallsign() {
return callsign;
}
public synchronized String getDestination() {
return destination;
}
}
}