/*

   Derby - Class org.apache.derby.shared.common.info.ProductVersionHolder

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to you under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

*/

package org.apache.derby.shared.common.info;

import java.io.InputStream;
import java.io.IOException;
import java.util.Properties;

import org.apache.derby.shared.common.error.StandardException;
import org.apache.derby.shared.common.info.JVMInfo;
import org.apache.derby.shared.common.reference.ModuleUtil;

/**
  Class to hold a Derby Product version.

  This class includes the following product version features.

  <OL>
  <LI>Save the product version information this holds as a String. We call
      the string a 'product version string'.
  <LI>Construct a ProductVersionHolder from a valid 'product version string'.
  <LI>Determine if two product versions are feature compatible. This means
      products of these versions may interoperate with ***NO*** compatibility
	  problems.
  <LI>Determine if two product versions are the same. This is a stronger
      test than the test for feature compatibility.
  </OL>



  Cloudscape 5.1 and older versions used the majorVersion, minorVersion, maintVersion versions
  directly. That is a three part version number, majorVersion.minorVersion.maintVersion, e.g. 5.1.21.

  For Cloudscape 5.2 onwards a four part name is required.
	majorVersion.minorVersion.fixPack.bugVersion e.g. 5.2.1.2

	This follows the IBM standard and allows us to state that a fix pack will be 5.2.3 without worrying
	about how many maintence fixes there are between fix packs.

	We implement this using the existing format of ProductVersionHolder to reduce disruption to the
	code, however we make the maintVersion encode the {fixPack.bugVersion}. Since the maintVersion
	is represented by a int (2G values) we have plenty of room for encoding. If we assign a given
	majorVersion.minorVersion.fixPack a 10 year life, then we about the maximum number of individual releases
	it can have is 10 years * 365 days/year = 3650. Thus with the pre 5.2 scheme we would not expect a 
	5.1.x to have an x &gt; 3650 (approximately). Usually the rate of point releases has been much less than
	one per day, 5.1.31 is released about 225 days after GA which makes around a point release every 7 days.
	But in the encoding we need to be conservative. With fix packs the maximum is about 2 per year and fix
	packs are only made to the current release, thus with a yearly minor release cycle we would imagine
	only 2 fixpacks per major.minor. However like other IBM products or release cycle may be extended thus
	we can expect up to a handful of fix packs.

	Thus we might imagine releases like

	5.2.0.12
	5.2.0.234
	5.2.1.34
	5.2.4.2445

  but highly unlikey to have

	5.2.2.59321
	5.2.23.1
	

    The encoding number must continue to increase so that the
	
		encodedMaintB &gt; encodedMaintA

		if (fixPackB &gt; fixPackA) || ((fixPackB == fixPackA) &amp;&amp; (bugB &gt; bugA))


	Selected encoding

	encodedMaint = (fixPack * 1,000,000) + (bugVersion);

	Handles many many fixpacks and upto one million bug fixes per fix pack and remains somewhat human readable.

	Special fix packs

	fixpack == 0 = alpha (version off main codeline)
	fixpack == 1 = first release of major.minor (may be marked with beta)
	fixpack == 2 = first fix pack (displayed as 1)
     

	The drdaMaintVersion is sent in the Network Server PRDID. It never displays
    but may be used by the client for version specific behaviour. It should be 
	reset to 0 with each minor release.	

  The product version string has the form:
  <PRE>
  productVendorName - ProductName - majorVersion.minorVersion.maintVersion [beta] - (buildNumber)

  </PRE>

  */
public final class ProductVersionHolder implements java.security.PrivilegedAction<Properties>
{

	//
	//Used as an invalid value for numbers. This works because all
	//the numbers in a product version must be non-negative.
	private static final int BAD_NUMBER = -1;
	private static final String ALPHA = "alpha";
	private static final String BETA = "beta";

	public final static int	MAINT_ENCODING = PropertyNames.MAINT_ENCODING;
	
	private String productVendorName;
	private String productName;
	private String productTechnologyName;
	private int majorVersion = BAD_NUMBER;
	private int minorVersion = BAD_NUMBER;
	private int maintVersion = BAD_NUMBER;
	private int drdaMaintVersion = BAD_NUMBER;
	private String buildNumber = "????";
	private Boolean isBeta;

	private ProductVersionHolder() {
	}
	
	/**
	  Create a ProductVersionHolder

	  <P>Please see the documentation for the varient of getProductVesionHolder
	  that takes the same parameters as this for a description of the parameters.
	  */
	private ProductVersionHolder(String productVendorName,
								 String productName,
								 String productTechnologyName,
								 int majorVersion,
								 int minorVersion,
								 int maintVersion,
								 int drdaMaintVersion,
								 String buildNumber,
								 Boolean isBeta)
	{
		if (productVendorName != null)
			this.productVendorName = productVendorName.trim();
		if (productName != null)
			this.productName = productName.trim();
		if (productTechnologyName != null)
			this.productTechnologyName = productTechnologyName.trim();
		this.majorVersion = majorVersion;
		this.minorVersion = minorVersion;
		this.maintVersion = maintVersion;
		this.drdaMaintVersion = drdaMaintVersion;
		this.buildNumber = buildNumber;
		this.isBeta = isBeta;
	}

	/**
	  Create a valid ProductVersionHolder. If any of the
	  parameters provided is invalid, this returns null.
	  @param productVendorName The name of the vendor
	  @param productName The name of the product. productName.length()
	  must be greater than 0. The syntax for a product name is
	  'productGenus[:productSpecies]'. 
	  @param productTechnologyName The technology name
	  @param majorVersion The most significant portion of a 3 
	  part product version.  Must be non-negative.
	  @param minorVersion The second portion of a 3 part 
	  product version. Must be non-negative.
	  @param maintVersion The least significant portion of a 3 part
	  product version. Must be non-negative.
	  @param drdaMaintVersion The protocol modification number for minor release.
	  @param buildNumber The buildNumber for a product. 
	  @param isBeta true iff the product is beta.
	  @return A valid ProductVersionHolder of null if any of the parameters
	  provided are not valid.
	  */
	public static ProductVersionHolder
	getProductVersionHolder(
						   String productVendorName,
						   String productName,
						   String productTechnologyName,
						   int majorVersion,
						   int minorVersion,
						   int maintVersion,
						   int drdaMaintVersion,
						   String buildNumber,
						   Boolean isBeta)
	{
		ProductVersionHolder pvh =
			new ProductVersionHolder(productVendorName,
									 productName,
									 productTechnologyName,
									 majorVersion,
									 minorVersion,
									 maintVersion,
									 drdaMaintVersion,
									 buildNumber,
									 isBeta);
		return pvh;
	}
	
	/**
	  Get a ProductVersionHolder for a product of a given genus,
	  that is available in the caller's environment. 
	  Even though this uses a priv bock, it may stil fail when
	  the jar the version is being fetched from, is different to the
	  one that loaded this class, AND the jars are in different security contexts.

	  @param productGenus The genus for the product.
	  @return The ProductVersionHolder or null if
	  a product with the given genus is not available in the
	  caller's environment.
	  */
	public static ProductVersionHolder
	getProductVersionHolderFromMyEnv(String productGenus)
	{

		ProductVersionHolder tempPVH = new ProductVersionHolder();

		tempPVH.productGenus = productGenus;
		Properties p = (Properties) java.security.AccessController.doPrivileged(tempPVH);

		if (p == null)
			return null;

		return getProductVersionHolder(p);
	}


	/**
		Load the version info from the already opened properties files.
		We need to do this because if the jar files (e.g. db2jtools and db2j)
		are in different security contexts (entries in the policy files) then
		we cannot load the version information for one of them correctly.
		This is because the this class will either have been loaded from
		only one of the jars and hence can only access the resource in its own jar.
		By making code specific to the jar open the resource we are guaranteed it will work.

        @param propertiesStream The version information

        @return the product descriptor
	*/
	public static ProductVersionHolder
	getProductVersionHolderFromMyEnv(InputStream propertiesStream)
	{

		if (propertiesStream == null)
			return null;

		Properties p = new Properties();
		try {
			p.load(propertiesStream);
		}
		catch (IOException ioe) {

			System.out.println("IOE " + ioe.getMessage());
			//
			//This case is a bit ugly. If we get an IOException, we return
			//null. Though this correctly reflects that the product is not
			//available for use, it may be confusing to users that we swallow
			//the IO error here.
			return null;
		} finally {
			try {
				propertiesStream.close();
			} catch (IOException ioe2) {
			}
		}

		return getProductVersionHolder(p);
	}

	/**
	  Get a ProductVersionHolder based on the information in
	  the Properties object provided.

	  @param p The properties object that holds the productVersion
	  information.
	  @return The ProductVersionHolder or null if
	  a product with the given genus is not available in the
	  caller's environment.
	  */
	public static ProductVersionHolder
	getProductVersionHolder(Properties p)
	{
		String pvn = p.getProperty(PropertyNames.PRODUCT_VENDOR_NAME);
		String pn = p.getProperty(PropertyNames.PRODUCT_EXTERNAL_NAME);
		String ptn = p.getProperty(PropertyNames.PRODUCT_TECHNOLOGY_NAME);
		int v1 = parseInt(p.getProperty(PropertyNames.PRODUCT_MAJOR_VERSION));
		int v2 = parseInt(p.getProperty(PropertyNames.PRODUCT_MINOR_VERSION));
		int v3 = parseInt(p.getProperty(PropertyNames.PRODUCT_MAINT_VERSION));
		int v4 = parseInt(p.getProperty(PropertyNames.PRODUCT_DRDA_MAINT_VERSION));
		String bn = p.getProperty(PropertyNames.PRODUCT_BUILD_NUMBER);
		Boolean isBeta =
			Boolean.valueOf(p.getProperty(PropertyNames.PRODUCT_BETA_VERSION));
		return 	getProductVersionHolder(pvn,pn,ptn,v1,v2,v3,v4,bn,isBeta);
	}


	/**
	  Return the product vendor name.

      @return the vendor name
	  */
	public String getProductVendorName()
	{
		return productVendorName;
	}


	/**
	  Return the external product name.

      @return the product name
	  */
	public String getProductName()
	{
		return productName;
	}
	public String getProductTechnologyName()
	{
		return productTechnologyName;
	}

	/**
	  Return the major version number.

      @return the major version number
	  */
	public int getMajorVersion() {return majorVersion;}
	/**
	  Return the minor version number.

      @return the minor version number
	  */
	public int getMinorVersion() {return minorVersion;}
	/**
	  Return the <B>encoded</B> maintainence version number.

      @return the maintenance version number
	  */
	public int getMaintVersion() {return maintVersion;}

	/** 
		Return the drda protocol maintenance version for this minor release.
		Starts at 0 for each minor release and only incremented 
		when client behaviour changes based on the server version.

        @return the drda maintenance version number
	**/
	public int getDrdaMaintVersion() {return drdaMaintVersion; }

	/**
       Return the fix pack version from the maintenance encoding.

       @return the fix pack version number
	*/
	public int getFixPackVersion() { return maintVersion / MAINT_ENCODING; }


	/**
	  Return true if this is a beta product.

      @return true if this is a beta product.
	  */
	public boolean isBeta() {return isBeta.booleanValue();}
	/**
	  Return true if this is a alpha product.

      @return true if this is a alpha product.
	  */
	public boolean isAlpha() {
		return	   (majorVersion >= 5)
				&& (minorVersion > 2)
				&& ((maintVersion / MAINT_ENCODING) == 0);
	}
	/**
	  Return the build number for this product.

      @return the build number for this product.
	  */
	public String getBuildNumber() {return buildNumber;}

    /**
     * Return the build number as an integer if possible,
     * mapping from the SVN number.
     * nnnnn -&gt; returns nnnnn
     * nnnnnM -&gt; returns -nnnnn indicates a modified code base
     * nnnnn:mmmmm -&gt; returns -nnnnn
     * anything else -&gt; returns -1
     *
     * @return the build number as an integer if possible,
    */
    public int getBuildNumberAsInt(){
    	if (buildNumber == null)
    	    return -1;
    	boolean dubiousCode = false;
    	int offset = buildNumber.indexOf('M');
    	if (offset == -1)
    	    offset = buildNumber.indexOf(':');
    	else
    	    dubiousCode = true;
    	if (offset == -1)
    		offset = buildNumber.length();
        else
            dubiousCode = true;
    	
    	try {
    		int bnai = Integer.parseInt(buildNumber.substring(0, offset));
    		if (dubiousCode)
    		    bnai = -bnai;
    		return bnai;
    	} catch (NumberFormatException nfe) 
     	{
     		return -1;
    	}
    }

	/**
	  Parse a string containing a non-negative integer. Return
	  a negative integer is the String is invalid.

	  @param s A string with a non-negative integer (a sequence
	  of decimal digits.)
	  @return the integer or a negative number if s is invalid.
	  */
	private static int parseInt(String s)
	{
		//System.out.println("Parsing integer: "+s);		
		int result = BAD_NUMBER;
		try
			{
				if (s!=null)
					result = Integer.parseInt(s);
			}
		catch (NumberFormatException nfe)
			{}

		if (result < 0) result = BAD_NUMBER;
		return result;
	}

	/**
	  Return  a string representation of this ProductVersion. The
	  difference between this and createProductVersionString, is
	  that this method retruns a String when this ProductVersionHolder
	  holds invalid version information.

      @return  a string representation of this ProductVersion.
	 */
	public String toString()
	{
		StringBuffer sb = new StringBuffer();
		sb.append(getProductVendorName());
		sb.append(" - ");
		sb.append(getProductName());
		sb.append(" - ");
		sb.append(getVersionBuildString(true));
		return sb.toString();
	}

	/**
		Return the feature version string, ie. major.minor. (e.g. 5.2)

        @return the feature version string, ie. major.minor. (e.g. 5.2)
	*/
	public String getSimpleVersionString() {

		return ProductVersionHolder.simpleVersionString(majorVersion, minorVersion, isBeta());
	}

	/**
		Convert a major and minor number with beta status into a string.

        @param major Major version number
        @param minor Minor version number
        @param isBeta Beta flag

        @return a version string encoding the arguments
	*/
	public static String simpleVersionString(int major, int minor, boolean isBeta) {

		StringBuffer sb = new StringBuffer();

		sb.append(major);
		sb.append('.');
		sb.append(minor);
		if (isBeta) {
			sb.append(' ');
			sb.append(BETA);
		}

		return sb.toString();
	}
	public static String fullVersionString(int major, int minor, int maint, boolean isBeta, String build) {
		StringBuffer sb = new StringBuffer();
		sb.append(major);
		sb.append('.');
		sb.append(minor);
		sb.append('.');

		String preRelease = null;
		if (major == 5 && minor <= 2 && maint < MAINT_ENCODING)
		{
			sb.append(maint);
			if (isBeta)
				preRelease = BETA;
		}
		else
		{
			int fixPack = maint / MAINT_ENCODING;
			int bugVersion = maint % MAINT_ENCODING;
			sb.append(fixPack);
			sb.append('.');
			sb.append(bugVersion);

			if (fixPack == 0)
			{
				preRelease = ALPHA;
			}
			else if (isBeta) {
				preRelease = BETA;
			}
		}

        if (preRelease != null)
        {
			sb.append(' ');
            sb.append(preRelease);
        }
		if (build != null) {
			sb.append(" - (");

			sb.append(build);
			sb.append(')');
		}
        return sb.toString();
	}
	/**
		Returns a short-hand value for the product version string.
		Used by Sysinfo.
		Includes the optional beta designation

        @param withBuild If true, include the build number

        @return the build version string
	*/
    public String getVersionBuildString(boolean withBuild)
    {
		return ProductVersionHolder.fullVersionString(majorVersion, minorVersion, maintVersion, isBeta(),
			withBuild ? buildNumber : null);
    }

	/*
	** Security related methods 
	*/
	private String productGenus;
	public final Properties run() {

		// SECURITY PERMISSION - IP4
		return loadProperties(this.productGenus);
	}
	// SECURITY PERMISSION - IP4
	private Properties loadProperties(String productGenus) {
		String resourceName = "/org/apache/derby/info/" + productGenus+"/info.properties";
			
		InputStream is = null;

        if (JVMInfo.isModuleAware())
        {
            try
            {
                is = ModuleUtil.getResourceAsStream(resourceName);
            }
            catch (StandardException ex)
            {
              // report some error if you can
              System.out.println(ex.getMessage());
            }
        }
        else { is = getClass().getResourceAsStream(resourceName); }
        
		if (is==null) {
			return null;
		}

		Properties p = new Properties();
		try {
			p.load(is);
			return p;
		}
		catch (IOException ioe) {
			//
			//This case is a bit ugly. If we get an IOException, we return
			//null. Though this correctly reflects that the product is not
			//available for use, it may be confusing to users that we swallow
			//the IO error here.
			return null;
		}
	}
}
