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;
 
   70     static ClosestCityMapper getInstance() throws IOException {
 
   71         if (instance == null) {
 
   72             instance = 
new ClosestCityMapper();
 
   79     private LatLngMap<CityRecord> latLngMap = null;
 
   82     private final java.util.logging.Logger logger;
 
   89     private ClosestCityMapper() throws IOException {
 
   91                 GeolocationSummary.class.getResourceAsStream(CITIES_CSV_FILENAME),
 
   92                 Logger.getLogger(ClosestCityMapper.class.getName()));
 
  104     private ClosestCityMapper(InputStream citiesInputStream, java.util.logging.Logger logger) throws IOException {
 
  105         this.logger = logger;
 
  106         latLngMap = 
new LatLngMap<CityRecord>(parseCsvLines(citiesInputStream, 
true));
 
  117     CityRecord findClosest(CityRecord point) {
 
  118         return latLngMap.findClosest(point);
 
  129     private Double tryParse(String s) {
 
  135             return Double.parseDouble(s);
 
  136         } 
catch (NumberFormatException ex) {
 
  150     private String parseCountryName(String orig, 
int lineNum) {
 
  151         if (StringUtils.isBlank(orig)) {
 
  152             logger.log(Level.WARNING, String.format(
"No country name determined for line %d.", lineNum));
 
  156         Matcher m = COUNTRY_WITH_COMMA.matcher(orig);
 
  158             return String.format(
"%s %s", m.group(1), m.group(2));
 
  173     private CityRecord getCsvCityRecord(List<String> csvRow, 
int lineNum) {
 
  174         if (csvRow == null || csvRow.size() <= MAX_IDX) {
 
  175             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)));
 
  180         String cityName = csvRow.get(CITY_NAME_IDX);
 
  181         if (StringUtils.isBlank(cityName)) {
 
  182             logger.log(Level.WARNING, String.format(
"No city name determined for line %d.", lineNum));
 
  187         String stateName = csvRow.get(STATE_NAME_IDX);
 
  188         String countryName = parseCountryName(csvRow.get(COUNTRY_NAME_IDX), lineNum);
 
  190         Double lattitude = tryParse(csvRow.get(LAT_IDX));
 
  191         if (lattitude == null) {
 
  192             logger.log(Level.WARNING, String.format(
"No lattitude determined for line %d.", lineNum));
 
  196         Double longitude = tryParse(csvRow.get(LONG_IDX));
 
  197         if (longitude == null) {
 
  198             logger.log(Level.WARNING, String.format(
"No longitude determined for line %d.", lineNum));
 
  202         return new CityRecord(cityName, stateName, countryName, lattitude, longitude);
 
  213     private List<String> parseCsvLine(String line, 
int lineNum) {
 
  214         if (line == null || line.length() <= 0) {
 
  215             logger.log(Level.INFO, String.format(
"Line at %d had no content", lineNum));
 
  219         List<String> allMatches = 
new ArrayList<String>();
 
  220         Matcher m = CSV_NAIVE_REGEX.matcher(line);
 
  222             allMatches.add(m.group(1));
 
  240     private List<CityRecord> parseCsvLines(InputStream csvInputStream, 
boolean ignoreHeaderRow) 
throws IOException {
 
  241         List<CityRecord> cityRecords = 
new ArrayList<>();
 
  242         try (BufferedReader reader = 
new BufferedReader(
new InputStreamReader(csvInputStream, 
"UTF-8"))) {
 
  244             String line = reader.readLine();
 
  246             if (line != null && ignoreHeaderRow) {
 
  247                 line = reader.readLine();
 
  251             while (line != null) {
 
  253                 List<String> rowElements = parseCsvLine(line, lineNum);
 
  255                 if (rowElements != null) {
 
  256                     cityRecords.add(getCsvCityRecord(rowElements, lineNum));
 
  259                 line = reader.readLine();