/*
 *    ProtocolPortProperties.java
 *    Copyright (C) 2008 New Zealand Digital Library, http://www.nzdl.org
 *
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 2 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program; if not, write to the Free Software
 *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.greenstone.util;

import java.util.Properties;

/*
 * Utility class to set the protocol and port values from the Properties instance provided,
 * which can be a Properties object loaded from either build.properties or global.properties
 * Currently used by Server3.java, ServletRealmCheck, FedoraServiceProxy and solr java code
 * GS2SolrSearch and SolrSearch.
 * (Could perhaps also be used by GlobalProperties in future, if applicable.)
 * Can adjust fallback behaviour in this one location to make them all work consistently.
 *
 * NOTE: although global.properties contains many additional programmatically set properties
 * that could simplify this class, the fact that this class should work for both build.properties
 * and global.properties means that this class will work only with the properties common to
 * both property files.
 */
public class ProtocolPortProperties {   

    public static final int ALL_CORRECT = 0;
    public static final int SECONDARY_PORT_NON_NUMERIC = 1;
    public static final int INVALID_PRIMARY_PORT = 2;
    public static final int NO_PROTOCOL = 3;
    public static final int INVALID_PROTOCOL = 4;
    public static final int NO_HTTP_PORT = 5;
    public static final int NO_HTTPS_PORT = 6;

    private static final String FALLBACK_PROTOCOL = "http";
    private static final String FALLBACK_HTTP_PORT = "8383";
    private static final String FALLBACK_HTTPS_PORT = "8443";

    private String protocol;
    private String port;
    private String httpPort;
    private String httpsPort;
    //private int portNum = -1; // port that matches protocol as int

    private String errorMsg;
    private int errorCode = ALL_CORRECT;

    private boolean httpRestrictedToLocal = true;
    private boolean supportsHttps = false;
    private String defaultPortPropertyName = "localhost.port.http";
    private String localHttpURL;

    // default protocol if multiple supported
    public String getProtocol() { return protocol; } 
    // default port (port for default protocol)
    public String getPort() { return port; }

    // httpPort is always set, but http may or may not be public
    public String getHttpPort() { return httpPort; }
    // httpsPort is null if https not supported
    public String getHttpsPort() { return httpsPort; }
    public int getPortNum() { return Integer.parseInt(port); }  

    // http is always available locally (over 127.0.0.1). But http may or may not be public.
    // Returns true if public http support requested
    public boolean supportsHttp() { return !httpRestrictedToLocal; }
    // true if https is supported
    public boolean supportsHttps() { return supportsHttps; }

    // used by Server3.java to know the name of the port property to load
    public String getDefaultPortPropertyName() { return defaultPortPropertyName; }

    public String getErrorMsg() { return errorMsg; }
    public int getErrorCode() { return errorCode; }

    public boolean hadError() { return errorCode != ALL_CORRECT; }

    // returns the local http base URL, something like http://127.0.0.1:<httpPort>
    public String getLocalHttpBaseAddress() {
	return localHttpURL;	
    }


    // Constructor that will throw an Exception on ports/protocol configuration error or inconsistency
    public ProtocolPortProperties(Properties props) throws Exception {
	this(props, false);
    }

    // If param recover is true, will attempt to fallback to sane values for protocol and ports.
    // You can still check for error by calling hadError(), getErrorMsg() and getErrorCode().
    // If param recover is false, will throw an Exception on bad ports/protocol configuration
    // and the error message is available as the exception description.
    public ProtocolPortProperties(Properties props, boolean recover) throws Exception
    {
	StringBuffer msg = new StringBuffer();
	
	httpPort = props.getProperty("localhost.port.http");
	if(httpPort == null || httpPort.equals("")) {
	    errorCode = NO_HTTP_PORT;
	    msg.append("compulsory property localhost.port.http has no value (must be set to a valid port number not already in use).");
	    httpPort = FALLBACK_HTTP_PORT;
	}
	
	// Setting the internal/local access url, which has to be over http (see
	// https://letsencrypt.org/docs/certificates-for-localhost/)
	// localhost.server.http defaults to 127.0.0.1 instead of localhost, since
	// localhost is unsafe as it can be mapped to something other than 127.0.0.1.
	localHttpURL = "http://" + props.getProperty("localhost.server.http", "127.0.0.1");
	if(!httpPort.equals("80")) {
	    localHttpURL = localHttpURL + ":" + httpPort;
	}

	String supportedProtocols = props.getProperty("server.protocols");
	if(supportedProtocols == null || supportedProtocols.equals("")) {
	    errorCode = NO_PROTOCOL;
	    msg.append("server.protocols not set.");
	    protocol = FALLBACK_PROTOCOL; // fallback to http as default (and sole) protocol
	    port = httpPort; // set default port to httpPort
	}
	else { // supportedProtocols was assigned something

	    if(!supportedProtocols.contains("http")) {
		errorCode = INVALID_PROTOCOL;
		msg.append("server.protocols must contain http or https, or both (in order of preference) separated by commas.");
		protocol = FALLBACK_PROTOCOL;
		port = httpPort;
	    } 

	    // set available protocols with their ports
	    if(supportedProtocols.contains("https")) {
		supportsHttps = true;
		httpsPort = props.getProperty("tomcat.port.https");
		if(httpsPort == null || httpsPort.equals("")) {
		    errorCode = NO_HTTPS_PORT;
		    msg.append("server.protocols includes https but property tomcat.port.https has no value (must be set to a valid port number not already in use).");
		    httpsPort = FALLBACK_HTTPS_PORT;
		}
	    }
	    if(supportedProtocols.matches("http(,|\\s+|$)")) {
		httpRestrictedToLocal = false;
	    }
	
	    // set default protocol and default port: the first protocol in the supportedProtocols list
	    if(supportedProtocols.matches("^[,\\s]*https")) {
		protocol = "https";
		port = httpsPort;		
	    } else {
		protocol = "http";
		port = httpPort;
	    }
	}

	if(!recover && errorCode == ALL_CORRECT) { // then check any assigned ports are valid
	    if(httpPort != null) {
		try {
		    Integer.parseInt(httpPort);
		} catch(NumberFormatException nfe) {
		    msg.append("\nInvalid localhost.port.http specified: must be numeric.");
		    if(port == httpPort) {
			errorCode = INVALID_PRIMARY_PORT;			
			port = httpPort = FALLBACK_HTTP_PORT; // recovery values that can work with default protocol			
		    } else { // secondary protocol's port is non-numeric, not fatal in recovery mode
			errorCode = SECONDARY_PORT_NON_NUMERIC;
			httpPort = null;
			if(recover) msg.append("\nNot using this port");
		    }
		}
	    }
	
	    if(httpsPort != null) {
		try {
		    Integer.parseInt(httpsPort);
		} catch(NumberFormatException nfe) {
		    msg.append("\nInvalid port specified for over https (tomcat.port.https): must be numeric.");
		    if(port == httpsPort) {
			errorCode = INVALID_PRIMARY_PORT;			
			port = httpsPort = FALLBACK_HTTPS_PORT; // recovery values that work with default protocol	
		    } else { // non primary port is invalid/non-numeric, not fatal in recovery mode
			errorCode = SECONDARY_PORT_NON_NUMERIC;
			httpsPort = null;
			if(recover) msg.append("\nNot using this port");
		    }
		}
	    }
	}
	
	// if the default port is the httpsPort, then modify the defaultPortPropertyName to match
	// (else it will remain at the property name for the http port)
	if(port == httpsPort) {
	    defaultPortPropertyName = "tomcat.port.https";
	}

	if(recover) {
	    msg.append("\nFalling back to port ").append(port).append(" and protocol ").append(protocol).append(".");
	}
	
	errorMsg = msg.toString();

	if(!recover && errorCode != ALL_CORRECT) {
	    throw new Exception(errorMsg);
	}
    }
    
}
