WQMTools.java [src/java/wqm/utils] Revision: 5268b41d84b2922476c91ba6d4917422097a888b  Date: Mon Nov 16 11:57:43 MST 2015
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package wqm.utils;

import csip.Config;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.apache.tomcat.jdbc.pool.PoolProperties;

/**
 *
 * @author Shaun Case
 */
public class WQMTools {

    // Only access these from a 'synchronized' member function...
    private static ConcurrentHashMap<String, DataSource> dataSources = new ConcurrentHashMap<>();
    private static ConcurrentHashMap<String, String> connectionStrings = new ConcurrentHashMap<>();

    public static String getR2GISFileURL(String path, String name) throws MalformedURLException {
	return ((("http://" + wqm.utils.WQMTools.getDatabaseHostname("r2gis") + "/r2/" + path + "/" + name + ".xml")).replace(" ", "%20")).replace("\\", "/");
    }

    //If at some point it becomes a performance issue, this method could be made to not be 'synchronized', as this syncs on the actual
    //  WQMTools as a whole.  A speedier implementation would be to synchronize() on the HashMaps, and use synchronized blocks of code.
    //  this could potentially allow for other threads looking for already built connections to continue with their work without having
    //  to wait on the threads looking for connections that have not yet been built and pooled, which is what happens now when the function
    //  in its entirety is synchronized.  If multiple hits per second are happening to the WQM service, then this might need to be done, as
    //  in testing the initial db connections prove to take around 1 sec, and once pooled take only milliseconds to return.
    public static synchronized Connection getConnection(String className, Logger logger) throws csip.ServiceException {
	Connection ret_val = null;
	boolean debugging = Boolean.parseBoolean(Config.getString("debugging", "false"));

	if (null != className) {
	    //Don't need to synchronize() around these calls because they are already in a synchronized member
	    if (!connectionStrings.containsKey(className)) {
		connectionStrings.put(className, Config.getString((debugging ? className.toLowerCase() + ".debug.db" : className.toLowerCase() + ".db")));

	    }

	    if (connectionStrings.get(className) == null || connectionStrings.get(className).isEmpty()) {
		if (logger != null) {
		    logger.log(Level.SEVERE, "Connection string not provided.  Please configure wqm.db configuration parameter.");
		}
		throw new csip.ServiceException("Unable to connect to WQM database.  Please check the " + (debugging ? className.toLowerCase() + ".debug.db" : className.toLowerCase() + ".db") + " configuration parameter");
	    }

	    try {
		if (!dataSources.containsKey(className)) {
		    createDataSource(className, Config.getString(className + ".db.driver", "org.postgresql.Driver"), logger);
		}

		ret_val = dataSources.get(className).getConnection();
	    } catch (SQLException ex) {
		if (logger != null) {
		    logger.log(Level.SEVERE, null, ex);
		}
		throw new csip.ServiceException("Failed to connect to WQM database.  Please check the " + className + " configuration parameter");
	    }
	}
	return ret_val;
    }

    private static String getDatabaseHostname(String className) throws MalformedURLException {
	String ret_val;
	boolean debugging = Boolean.parseBoolean(Config.getString("debugging", "false"));

	ret_val = new URL(Config.getString((debugging ? className + ".debug.db" : className + ".db")).replace("jdbc:postgresql", "http")).getHost();

	return ret_val;
    }

    //Doesn't 'need' to be synchronized because it should only be called by getConnection() which is synchronized, 
    //  but just in case someone tries calling it elsewhere within this class at a later date...leave it synchronized.
    private static synchronized void createDataSource(String className, String DriverClassName, Logger logger) throws SQLException {
	PoolProperties p = new PoolProperties();
	p.setUrl(connectionStrings.get(className));
	//p.setDefaultReadOnly( true );
	p.setDriverClassName(DriverClassName);
	p.setJmxEnabled(false);
	p.setTestOnBorrow(true);
	p.setValidationQuery("SELECT 1");
	p.setTestOnReturn(false);
	p.setValidationInterval(30000);
	p.setTimeBetweenEvictionRunsMillis(30000);
	p.setMaxWait(10000);
	p.setRemoveAbandonedTimeout(30);  //Experimentally find an optimal value for this...
	p.setRemoveAbandoned(true);
	p.setJdbcInterceptors("org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;"
		+ "org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer;"
		+ "org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer");

	dataSources.put(className, new org.apache.tomcat.jdbc.pool.DataSource(p));
	if (logger != null) {
	    logger.log(Level.INFO, "\nCreated datasource {0}", dataSources.get(className).toString());
	}
    }

    /*
     public static void shutdownDataSource() {
     if (datasourceR2GIS != null && datasourceR2GIS instanceof DataSourceProxy) {
     DataSourceProxy proxy = (DataSourceProxy) datasourceR2GIS;
     proxy.close(true);
     datasourceR2GIS = null;
     }
        
     if (datasourceSSURGO != null && datasourceSSURGO instanceof DataSourceProxy) {
     DataSourceProxy proxy = (DataSourceProxy) datasourceSSURGO;
     proxy.close(true);
     datasourceSSURGO = null;
     }
        
     if (datasourceWQM != null && datasourceWQM instanceof DataSourceProxy) {
     DataSourceProxy proxy = (DataSourceProxy) datasourceWQM;
     proxy.close(true);
     datasourceWQM = null;
     }        
     } 
     */
    public static class PolygonLatLon {

	private ArrayList<WQMTools.PolygonLatLon.PointLatLon> points;
	private Boolean bValid;

	public PolygonLatLon() {
	    this.bValid = true;
	    this.points = new ArrayList<>();
	}

	public PolygonLatLon(ArrayList<WQMTools.PolygonLatLon.PointLatLon> tPoints) {
	    if (null != tPoints) {
		this.bValid = true;
		this.points = new ArrayList<>(tPoints);
		for (WQMTools.PolygonLatLon.PointLatLon tPoint : tPoints) {
		    if (!tPoint.isValid()) {
			this.bValid = false;
			break;
		    }
		}
	    }
	}

	public void add(WQMTools.PolygonLatLon.PointLatLon tPoint) {
	    this.points.add(tPoint);
	    if (!tPoint.isValid()) {
		this.bValid = false;
	    }
	}

	public void add(Double Lat, Double Lon) {
	    WQMTools.PolygonLatLon.PointLatLon tPoint = new WQMTools.PolygonLatLon.PointLatLon(Lat, Lon);
	    this.points.add(tPoint);
	    if (!tPoint.isValid()) {
		this.bValid = false;
	    }
	}

	public String toWKT() {
	    String ret_val = "";
	    int currentPoint = 0;
	    StringBuilder buff = new StringBuilder();

	    if (this.bValid) {
		buff.append("'POLYGON((");
		for (WQMTools.PolygonLatLon.PointLatLon tPoint : this.points) {
		    if (currentPoint > 0) {
			buff.append(", ");
		    }
		    //  Remember Lon is X and Lat is Y...
		    buff.append(tPoint.getLon().toString());
		    buff.append(" ");
		    buff.append(tPoint.getLat().toString());
		    //ret_val += tPoint.getLon().toString() + " " + tPoint.getLat().toString();
		    currentPoint++;
		}

		buff.append("))'");
		ret_val = buff.toString();
	    }

	    return ret_val;
	}

	public Boolean isValid() {
	    return this.bValid;
	}

	public static class PointLatLon {

	    private Double Lat;
	    private Double Lon;
	    private Boolean bValid;

	    public PointLatLon(Double Lat, Double Lon) {
		this.Lat = Lat;
		this.Lon = Lon;
		this.bValid = true;

		if (Math.abs(Lon) > 180) {
		    this.bValid = false;
		}

		if (Math.abs(Lat) > 90) {
		    this.bValid = false;
		}
	    }

	    public Boolean isValid() {
		return this.bValid;
	    }

	    public Double getLat() {
		return this.Lat;
	    }

	    public Double getLon() {
		return this.Lon;
	    }

	    public void setLon(Double Lon) {
		this.Lon = Lon;
		if (Math.abs(Lon) > 180) {
		    this.bValid = false;
		}
	    }

	    public void setLat(Double Lat) {
		this.Lat = Lat;
		if (Math.abs(Lat) > 90) {
		    this.bValid = false;
		}
	    }
	}
    }
}