19 package org.sleuthkit.autopsy.datasourcesummary.datamodel;
 
   21 import java.io.BufferedReader;
 
   22 import java.io.IOException;
 
   23 import java.io.InputStream;
 
   24 import java.io.InputStreamReader;
 
   25 import java.util.ArrayList;
 
   26 import java.util.List;
 
   27 import java.util.logging.Level;
 
   28 import java.util.regex.Matcher;
 
   29 import java.util.regex.Pattern;
 
   30 import java.util.stream.Stream;
 
   31 import org.apache.commons.lang3.StringUtils;
 
   38 class ClosestCityMapper {
 
   41     private static final String CITIES_CSV_FILENAME = 
"worldcities.csv";
 
   44     private static final int CITY_NAME_IDX = 0;
 
   45     private static final int STATE_NAME_IDX = 7;
 
   46     private static final int COUNTRY_NAME_IDX = 4;
 
   47     private static final int LAT_IDX = 2;
 
   48     private static final int LONG_IDX = 3;
 
   51     private static final Pattern CSV_NAIVE_REGEX = Pattern.compile(
"\"\\s*(([^\"]+?)?)\\s*\"");
 
   54     private static final Pattern COUNTRY_WITH_COMMA = Pattern.compile(
"^\\s*([^,]*)\\s*,\\s*([^,]*)\\s*$");
 
   56     private static final int MAX_IDX = Stream.of(CITY_NAME_IDX, STATE_NAME_IDX, COUNTRY_NAME_IDX, LAT_IDX, LONG_IDX)
 
   57             .max(Integer::compare)
 
   61     private static ClosestCityMapper instance = null;
 
   69     static ClosestCityMapper getInstance() throws IOException {
 
   70         if (instance == null) {
 
   71             instance = 
new ClosestCityMapper();
 
   78     private LatLngMap<CityRecord> latLngMap = null;
 
   81     private final java.util.logging.Logger logger;
 
   88     private ClosestCityMapper() throws IOException {
 
   90                 GeolocationSummary.class.getResourceAsStream(CITIES_CSV_FILENAME),
 
   91                 Logger.getLogger(ClosestCityMapper.class.getName()));
 
  102     private ClosestCityMapper(InputStream citiesInputStream, java.util.logging.Logger logger) throws IOException {
 
  103         this.logger = logger;
 
  104         latLngMap = 
new LatLngMap<CityRecord>(parseCsvLines(citiesInputStream, 
true));
 
  114     CityRecord findClosest(CityRecord point) {
 
  115         return latLngMap.findClosest(point);
 
  125     private Double tryParse(String s) {
 
  131             return Double.parseDouble(s);
 
  132         } 
catch (NumberFormatException ex) {
 
  145     private String parseCountryName(String orig, 
int lineNum) {
 
  146         if (StringUtils.isBlank(orig)) {
 
  147             logger.log(Level.WARNING, String.format(
"No country name determined for line %d.", lineNum));
 
  151         Matcher m = COUNTRY_WITH_COMMA.matcher(orig);
 
  153             return String.format(
"%s %s", m.group(1), m.group(2));
 
  167     private CityRecord getCsvCityRecord(List<String> csvRow, 
int lineNum) {
 
  168         if (csvRow == null || csvRow.size() <= MAX_IDX) {
 
  169             logger.log(Level.WARNING, String.format(
"Row at line number %d is required to have at least %d elements and does not.", lineNum, (MAX_IDX + 1)));
 
  174         String cityName = csvRow.get(CITY_NAME_IDX);
 
  175         if (StringUtils.isBlank(cityName)) {
 
  176             logger.log(Level.WARNING, String.format(
"No city name determined for line %d.", lineNum));
 
  181         String stateName = csvRow.get(STATE_NAME_IDX);
 
  182         String countryName = parseCountryName(csvRow.get(COUNTRY_NAME_IDX), lineNum);
 
  184         Double lattitude = tryParse(csvRow.get(LAT_IDX));
 
  185         if (lattitude == null) {
 
  186             logger.log(Level.WARNING, String.format(
"No lattitude determined for line %d.", lineNum));
 
  190         Double longitude = tryParse(csvRow.get(LONG_IDX));
 
  191         if (longitude == null) {
 
  192             logger.log(Level.WARNING, String.format(
"No longitude determined for line %d.", lineNum));
 
  196         return new CityRecord(cityName, stateName, countryName, lattitude, longitude);
 
  206     private List<String> parseCsvLine(String line, 
int lineNum) {
 
  207         if (line == null || line.length() <= 0) {
 
  208             logger.log(Level.INFO, String.format(
"Line at %d had no content", lineNum));
 
  212         List<String> allMatches = 
new ArrayList<String>();
 
  213         Matcher m = CSV_NAIVE_REGEX.matcher(line);
 
  215             allMatches.add(m.group(1));
 
  231     private List<CityRecord> parseCsvLines(InputStream csvInputStream, 
boolean ignoreHeaderRow) 
throws IOException {
 
  232         List<CityRecord> cityRecords = 
new ArrayList<>();
 
  233         try (BufferedReader reader = 
new BufferedReader(
new InputStreamReader(csvInputStream, 
"UTF-8"))) {
 
  235             String line = reader.readLine();
 
  237             if (line != null && ignoreHeaderRow) {
 
  238                 line = reader.readLine();
 
  242             while (line != null) {
 
  244                 List<String> rowElements = parseCsvLine(line, lineNum);
 
  246                 if (rowElements != null) {
 
  247                     cityRecords.add(getCsvCityRecord(rowElements, lineNum));
 
  250                 line = reader.readLine();