GeoData.java

package net.nulll.uso.iPAVGeoGrabber;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import net.nulll.uso.iPAddressViewer.LogItem;
import net.nulll.uso.iPAddressViewer.Msg;
import net.nulll.uso.iPAddressViewer.Uso;
import net.nulll.uso.iPAddressViewer.iPAddressViewer;

import org.bukkit.Bukkit;

public class GeoData {

   String city = null;
   String state = null;
   String country = null;

   GeoData(final LogItem logItem, final String addressString){
   //GeoData(final LogItem logItem, final String address){//For testing specific addresses
      //final String addressString = "8.8.8.8";//For testing specific addresses
      
      //Create a list of data sources that haven't exceeded their limits
      List<DataSource> availableSourceList = new ArrayList<DataSource>();
      long time = new Date().getTime();
      for(DataSource source : iPAVGeoGrabber.dataSources){
         //If the data source's session has ended, reset it's query count and failures, then start a new session
         if(source.getSessionTime() + source.getSessionDuration() < time){
            source.setCurrentQueries(0);
            source.setFailures(0);
            source.updateSessionTime(time);
         }
         //If the source hasn't been queried too much within its current session, make it available on the source list
         if(source.getCurrentQueries() < source.getMaxQueries() || source.getMaxQueries() < 0){
            availableSourceList.add(source);
         }
      }
      //Create an ordered list of data sources by using their failure count
      //If a source is failing to give the geo data, it will fall down the list and allow other sources to be queried first instead
      final List<DataSource> orderedSourceList = new ArrayList<DataSource>();
      for(DataSource source : availableSourceList){
         if(orderedSourceList.size()==0){orderedSourceList.add(source); continue;}
         for(int i = 0; i < orderedSourceList.size(); i++){
            if(source.getFailures() < orderedSourceList.get(i).getFailures()){
               orderedSourceList.add(i, source);
               break;
            }else if(i==orderedSourceList.size()-1){
               orderedSourceList.add(source);
               break;
            }
         }
      }
      
      //Create an asynchronous task so that the main server thread isn't held up while the data is being acquired from the web source
      Bukkit.getScheduler().runTaskAsynchronously(iPAddressViewer.staticPlugin, new Runnable(){public void run(){
         //Loop through all of the data sources. Hopefully the first source (which has the lowest failure count) will be successful
         //If it isn't, the next source will be queried
         for(final DataSource source : orderedSourceList){
            //Catch any errors that occur while downloading from the current source and searching for geo data on it
            //That way the search continues and the other sources can be queried too
            try{
               boolean validAddress = false;
               //The website is about to be downloaded from, so the query count must be increased
               //Downloading too much from one site in a certain time-frame (session) might make them blacklist your server address from asking them for info
               source.setCurrentQueries(source.getCurrentQueries()+1);
               //Schedule a task to save the data source statistics in the main server thread. Asynchronously saving can corrupt the data file.
               Bukkit.getScheduler().runTask(iPAddressViewer.staticPlugin, new Runnable(){public void run(){
                  //Use a saving utility that iPAV has in order to save iPAVGeoGrabber's data file
                  Uso.saveYaml(iPAVGeoGrabber.geoStats, iPAddressViewer.staticPlugin.getDataFolder() + "/geoSourceStats.ipav");
               }});
               //Build the website URL to access, open the connection, set the download fail time to 5 seconds, pretend we're a web browser, then start reading
               URL lookupURL = new URL(source.getURLWithAddress(addressString));
                  URLConnection connection = lookupURL.openConnection();
                  connection.setReadTimeout(5000);
                  connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB;     rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13 (.NET CLR 3.5.30729)");
                  BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
                 String line = in.readLine();
                 while(line != null){
                    //Check the source's yamlKey so that we know which website we're downloading from. That way we know where to get the data we want on their site
                     if(source.yamlKey.equals("ipTracker")){
                       if(line.toLowerCase().contains("country")){
                          validAddress = true;
                       }
                       if(line.contains("<th>Country:</th>")){
                           String[] contentArray = line.split("<|>");
                           for(int i = 0; i < contentArray.length; i++){
                              if(contentArray[i].contains("Country:")){
                                 if(i+4 < contentArray.length){
                                    country = contentArray[i+4].replace("&nbsp;", "").trim();
                                 }
                              }else if(contentArray[i].contains("State:")){
                                 if(i+4 < contentArray.length){
                                    state = contentArray[i+4].trim();
                                 }
                              }else if(contentArray[i].contains("City Location:")){
                                 if(i+4 < contentArray.length){
                                    city = contentArray[i+4].trim();
                                 }
                              }
                              if(country != null && state != null && city != null){
                                 break;
                              }
                           }
                        }
                    }else if(source.yamlKey.equals("whatismyipaddress")){
                       if(line.toLowerCase().contains("country")){
                          validAddress = true;
                       }
                       if(line.contains("<th>Country:</th>")){
                           String[] contentArray = line.split("<|>");
                           for(int i = 0; i < contentArray.length; i++){
                              if(contentArray[i].contains("Country:")){
                                 if(i+4 < contentArray.length){
                                    country = contentArray[i+4].replace("&nbsp;", "").trim();
                                    break;
                                 }
                              }
                           }
                        }
                       if(line.contains("<th>State/Region:</th>")){
                           String[] contentArray = line.split("<|>");
                           for(int i = 0; i < contentArray.length; i++){
                              if(contentArray[i].contains("State/Region:")){
                                 if(i+4 < contentArray.length){
                                    state = contentArray[i+4].trim();
                                    break;
                                 }
                              }
                           }
                        }
                       if(line.contains("<th>City:</th>")){
                           String[] contentArray = line.split("<|>");
                           for(int i = 0; i < contentArray.length; i++){
                              if(contentArray[i].contains("City:")){
                                 if(i+4 < contentArray.length){
                                    city = contentArray[i+4].trim();
                                    break;
                                 }
                              }
                           }
                        }
                    }else if(source.yamlKey.equals("ipinfo")){
                       if(line.toLowerCase().matches(" +\"country\": \"\\w{2,}\",\\n")){
                          validAddress = true;
                       }
                       if(line.startsWith("  \"city\": \"")){
                          city = line.replace("  \"city\": \"", "").replace("\",", "");
                       }else if(line.startsWith("  \"region\": \"")){
                          state = line.replace("  \"region\": \"", "").replace("\",", "");
                       }else if(line.startsWith("  \"country\": \"")){
                          country = line.replace("  \"country\": \"", "").replace("\",", "");
                       }
                    }else if(source.yamlKey.equals("hackertarget")){
                       if(line.toLowerCase().matches("country: \\w{2,}\\n")){
                          validAddress = true;
                       }
                       if(line.startsWith("City: ")){
                          city = line.replace("City: ", "");
                       }else if(line.startsWith("State: ")){
                          state = line.replace("State: ", "");
                       }else if(line.startsWith("Country: ")){
                          country = line.replace("Country: ", "");
                       }
                    }
                    
                    //Check to see if all of the geo data has been collected. If so, stop reading the site data
                    if(country != null && state != null && city != null){
                        break;
                     }
                    
                    //Read the next line of site data
                     line = in.readLine();
                 }
                 
                 //Close the input stream that was opened earlier
                 in.close();
                 
                 //If the geo data has been successfully acquired, format it a little and then forward it to iPAV
                 if(geoDataFound()){
                    //We're done downloading, now we want to go back into the main server thread so that anything iPAV does with the data is in-sync
                    Bukkit.getScheduler().runTask(iPAddressViewer.staticPlugin, new Runnable(){public void run(){
                       //Some sources give a two-letter country code instead of the full country name, so we need to translate it using the hash map of country codes
                        if(country.length() <= 3 && iPAVGeoGrabber.countryCodeMap.get(country.toUpperCase()) != null){
                           country = iPAVGeoGrabber.countryCodeMap.get(country.toUpperCase());
                        }
                        if(city.toLowerCase().matches("unknown|n/a")){city = "?";}
                        if(state.toLowerCase().matches("unknown|n/a")){state = "?";}
                        //Update the log to have the new geo location data
                        logItem.applyGeoData(city, state, country);
                        //Send the updated log to iPAV so that it can save it and show it to admins
                      iPAddressViewer.forwardGeoData(logItem);
                    }});
                    //The data we wanted has been found and iPAV received it. We no longer want to query the other sources, so it's time to exit
                    return;
                 }else{
                    //If the page didn't have the data we wanted, add a failure for it.
                    //If the current data source is failing, other data sources will get priority over it
                    retrievalFailure(source, addressString);
                    if(validAddress){
                       //Record the failure for this data source. If it's failing now, other data sources will get priority over it
                    }else{
                       //For whatever reason, the data source didn't even look like it had the data we wanted.
                       //This might have been because of the address we tried to search for, so other addresses might be fine.
                       Bukkit.getScheduler().runTask(iPAddressViewer.staticPlugin, new Runnable(){public void run(){
                          iPAddressViewer.forwardGeoData(logItem);
                       }});
                       return;
                    }
               }
            }catch(Exception e){
               //There was an error while trying to read or download the website data
               //Tell the server owner why geo data wasn't retrieved from the current source
               e.printStackTrace();
               //Record this failure for the data source
               retrievalFailure(source, addressString);
            }
         }
         //Even after searching though all of our data sources, we didn't get the geo location data
         //iPAV still needs to receive the log back so that it can show admins the info about the user that is connecting
         Bukkit.getScheduler().runTask(iPAddressViewer.staticPlugin, new Runnable(){public void run(){
             iPAddressViewer.forwardGeoData(logItem);
          }});
      }});
   }
   
   private void retrievalFailure(final DataSource source, final String address){
      Bukkit.getScheduler().runTask(iPAddressViewer.staticPlugin, new Runnable(){public void run(){
         source.setFailures(source.getFailures()+1);
         Msg.msgC("&4[iPAVGeoGrabber] Unable to retrieve geo data from " + source.getURLWithAddress(address));
         Uso.saveYaml(iPAVGeoGrabber.geoStats, iPAddressViewer.staticPlugin.getDataFolder() + "/geoSourceStats.ipav");
      }});
   }
   
   boolean geoDataFound(){
      //If we found at least one part of geo data for the address, then...
      if((city != null && city.length() >= 2) || (state != null && state.length() >= 2) || (country != null && country.length() >= 2)){
         //Make sure the other pieces of data won't cause errors later on for being invalid
         if(city==null || city.equals("")){city = "?";}
         if(state==null || state.equals("")){state = "?";}
         if(country==null || country.equals("")){country = "?";}
         return true;
      }
      return false;
   }
}