/*******************************************************************************
 * Copyright (c) 2003, 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.framework.internal.core;

import java.util.Enumeration;
import java.util.Vector;
import org.eclipse.osgi.service.resolver.VersionRange;
import org.eclipse.osgi.util.ManifestElement;
import org.osgi.framework.*;
import org.osgi.framework.Constants;

/**
 * This class represents a description of native code.
 * 
 * Native Code dependencies
 * 
 * The Bundle-NativeCode header allows a bundle to carry the native code it
 * needs, and make use of it when it is installed. The bundle must have
 * RuntimePermission in order to run native code in the Framework. The value of
 * the header must conform to the following syntax:
 * 
 * 
 * Bundle-NativeCode: nativecode-clause ( , nativecode-clause)*
 * nativecode-clause: nativepaths ( ; env-parameter )* nativepaths: nativepath ( ;
 * nativepath )* env-parameter: ( processordef | osnamedef | osversiondef |
 * languagedef ) processordef: <I>processor= </I>token osnamedef: <I>osname=
 * </I>token osversiondef: <I>osversion= </I>token languagedef: <I>language=
 * </I>token
 * 
 * For example:
 * 
 * Bundle-NativeCode: http.dll ; osname=Win95; processor=x86; language=en,
 * libhttp.so; osname=Solaris; processor=sparc
 * 
 * The Bundle-NativeCode header allows a bundle programmer to specify an
 * environment, and to declare what native code libraries it carries for that
 * specific environment. The environment is characterized by the following
 * properties:
 * 
 * <UL>
 * <LI><CODE>processor</CODE> The processor on which the hosting the
 * Framework is running. It is compared against <CODE>
 * org.osgi.framework.processor</CODE>.
 * <LI><CODE>osname</CODE> The operating system name. It is compared against
 * <CODE>org.osgi.framework.os.name</CODE>.
 * <LI><CODE>selection-filter</CODE> An optional filter that can be used to
 * match against system properties. If the filter does not match then the
 * native code clause will not be selected.
 * <LI><CODE>osversion</CODE> The version of the operating system. It is
 * compared against <CODE>org.osgi.framework.os.version</CODE>.
 * <LI><CODE>language</CODE> The language. It is compared against <CODE>
 * org.osgi.framework.language</CODE>.
 * </UL>
 * 
 * These properties should follow the conventions and values defined in the
 * "Open Software Description" specification.
 * 
 * The Framework uses the following algorithm to find the "best" matching
 * native code clause:
 * 
 * <ol>
 * <li>Pick the clauses with a matching processor and operating system with
 * the one the Framework runs on. If no clause matches both the required
 * processor and operating system, the bundle installation/activation fails. If
 * only one clause matches, it can be used, otherwise, remaining steps are
 * executed.</li>
 * <li>Pick the clauses that best match the operating system version. If they
 * match each other exactly, that clause is considered the best match. If there
 * is only one clause with an exact match, it can be used. If there are more
 * than one clause that matches the property, these clauses will be picked to
 * perform the next step. Operating system versions are taken to be backward
 * compatible. If there is no exact match in the clauses, clauses with
 * operating system versions lower than the value specified in
 * org.osgi.framework.osversion will be picked. If there is only one clause
 * which has a compatible operating system version, it can be used. Otherwise,
 * all clauses with compatible operating system versions will go through the
 * next step. If no clause has a matching or compatible operating system
 * version, pick the clause that does not have operating system version
 * specified. If that is not possible, the bundle installation fails.</li>
 * <li>Pick the clause that best matches the language. If more than one clause
 * remains at that point, then the Framework is free to pick amongst them
 * randomly. If no clauses have the exact match with the value of the property,
 * pick the clause that does not have language specified. If that is not
 * possible, the bundle installation fails.</li>
 * </ol>
 *  
 */
public class BundleNativeCode {
	/**
	 * The Native Code paths for the Native Code entry
	 */
	private Attribute nativepaths;
	/**
	 * The processor attribute for this Native Code entry
	 */
	private Attribute processor;
	/**
	 * The osname attribute for this Native Code entry
	 */
	private Attribute osname;
	/**
	 * The language attribute for this Native Code entry
	 */
	private Attribute language;
	/**
	 * The osversion attribute for this Native Code entry
	 */
	private Attribute osversion;
	/**
	 * The filter attribute for this Native Code entry
	 */
	private String filterString;
	/**
	 * The Framework for this BundleNativeCode
	 */
	private AbstractBundle bundle;

	/**
	 * The AliasMapper used to alias OS Names.
	 */
	private static AliasMapper aliasMapper = Framework.aliasMapper;

	/**
	 * Constructor for BundleNativeCode. It reads bundle native code data from
	 * the manifest file.
	 *  
	 */
	protected BundleNativeCode(ManifestElement element, AbstractBundle bundle) {
		this.bundle = bundle;
		String[] nativePaths = element.getValueComponents();
		for (int i = 0; i < nativePaths.length; i++) {
			addPath(nativePaths[i]);
		}
		setAttribute(element, Constants.BUNDLE_NATIVECODE_OSNAME);
		setAttribute(element, Constants.BUNDLE_NATIVECODE_PROCESSOR);
		setAttribute(element, Constants.BUNDLE_NATIVECODE_OSVERSION);
		setAttribute(element, Constants.BUNDLE_NATIVECODE_LANGUAGE);
		setAttribute(element, Constants.SELECTION_FILTER_ATTRIBUTE);
	}

	private void setAttribute(ManifestElement element, String attribute) {
		String[] attrValues = element.getAttributes(attribute);
		if (attrValues != null) {
			for (int i = 0; i < attrValues.length; i++) {
				addAttribute(attribute, attrValues[i]);
			}
		}
	}

	/**
	 * Returns the native code paths.
	 * 
	 * @return Vector of String code paths.
	 */
	public String[] getPaths() {
		if (nativepaths == null) {
			return null;
		}
		String[] paths = new String[nativepaths.size()];
		nativepaths.toArray(paths);
		return (paths);
	}

	/**
	 * addPath is used to add a new element to the list of native files.
	 * 
	 * @param nativepath
	 *            new native file
	 */
	protected void addPath(String nativepath) {
		if (nativepaths == null) {
			nativepaths = new Attribute();
		}
		nativepaths.addElement(nativepath);
	}

	/**
	 * addAttribute is used to add the specification-version string to the
	 * package description. It is the only key supported at this time.
	 * 
	 * @param key
	 *            attribute key name
	 * @param value
	 *            attribute value name
	 */
	protected synchronized void addAttribute(String key, String value) {
		if (key.equals(Constants.BUNDLE_NATIVECODE_PROCESSOR)) {
			if (processor == null) {
				processor = new Attribute();
			}
			processor.addElement(aliasMapper.aliasProcessor(value));
			return;
		}
		if (key.equals(Constants.BUNDLE_NATIVECODE_OSNAME)) {
			if (osname == null) {
				osname = new Attribute();
			}
			osname.addElement(aliasMapper.aliasOSName(value));
			return;
		}
		if (key.equals(Constants.BUNDLE_NATIVECODE_OSVERSION)) {
			if (osversion == null) {
				osversion = new Attribute();
			}
			osversion.addElement(new VersionRange(value));
			return;
		}
		if (key.equals(Constants.SELECTION_FILTER_ATTRIBUTE)) {
			if (filterString == null) {
				filterString = value;
			}
			return;
		}
		if (key.equals(Constants.BUNDLE_NATIVECODE_LANGUAGE)) {
			if (language == null) {
				language = new Attribute();
			}
			language.addElement(value.toLowerCase());
			return;
		}
	}

	/**
	 * Override toString. Return a String representation of this object
	 * 
	 * @return String representation of the object
	 */
	public String toString() {
		int size = nativepaths.size();
		StringBuffer sb = new StringBuffer(50 * size);
		for (int i = 0; i < size; i++) {
			if (i > 0) {
				sb.append(';');
			}
			sb.append(nativepaths.elementAt(i).toString());
		}
		if (processor != null) {
			size = processor.size();
			for (int i = 0; i < size; i++) {
				sb.append(';');
				sb.append(Constants.BUNDLE_NATIVECODE_PROCESSOR);
				sb.append('=');
				sb.append(processor.elementAt(i).toString());
			}
		}
		if (osname != null) {
			size = osname.size();
			for (int i = 0; i < size; i++) {
				sb.append(';');
				sb.append(Constants.BUNDLE_NATIVECODE_OSNAME);
				sb.append('=');
				sb.append(osname.elementAt(i).toString());
			}
		}
		if (osversion != null) {
			size = osversion.size();
			for (int i = 0; i < size; i++) {
				sb.append(';');
				sb.append(Constants.BUNDLE_NATIVECODE_OSVERSION);
				sb.append('=');
				sb.append(osversion.elementAt(i).toString());
			}
		}
		if (language != null) {
			size = language.size();
			for (int i = 0; i < size; i++) {
				sb.append(';');
				sb.append(Constants.BUNDLE_NATIVECODE_LANGUAGE);
				sb.append('=');
				sb.append(language.elementAt(i).toString());
			}
		}
		return (sb.toString());
	}

	/**
	 * Return the match value for the given processor and os name. A higher
	 * value indicates a better match.
	 * 
	 * @param processor
	 *            processor name to match against.
	 * @param osname
	 *            os name to match against.
	 * @return match value
	 */
	public int matchProcessorOSNameFilter(String processor, String osname) {
		if ((this.processor == null) || (this.osname == null)) {
			return (0);
		}
		String otherProcessor = aliasMapper.aliasProcessor(processor);
		String otherOSName = (String) aliasMapper.aliasOSName(osname);
		if (this.processor.equals(otherProcessor) && this.osname.equals(otherOSName) && matchFilter()) {
			return (1);
		}
		return (0);
	}

	/**
	 * Return the higest matching value for the given os version that is less
	 * than or equal to the given os version.
	 * 
	 * @param version
	 *            os version to match against.
	 * @return version or null if no match.
	 */
	public Version matchOSVersion(Version version) {
		if (this.osversion == null)
			return Version.emptyVersion;
		Version result = null;
		int size = this.osversion.size();
		for (int i = 0; i < size; i++) {
			// find a matching range and save the highest result
			VersionRange range = (VersionRange) this.osversion.elementAt(i);
			if (range.isIncluded(version) && (result == null || (range.getMinimum().compareTo(result) > 0)))
				result = range.getMinimum();
		}
		return result;
	}

	/**
	 * Return the match value for the given language. A higher value indicates
	 * a better match.
	 * 
	 * @param language
	 *            language name to match against.
	 * @return match value
	 */
	public int matchLanguage(String language) {
		if (this.language == null) {
			return (1);
		}
		if (this.language.equals(language.toLowerCase())) {
			return (2);
		}
		return (0);
	}

	public boolean matchFilter() {
		if (filterString == null) {
			return true;
		}
		FilterImpl filter;
		try {
			filter = new FilterImpl(filterString);
		} catch (InvalidSyntaxException e) {
			BundleException be = new BundleException(Msg.BUNDLE_NATIVECODE_INVALID_FILTER, e); //$NON-NLS-1$
			bundle.framework.publishFrameworkEvent(FrameworkEvent.ERROR, bundle, be);
			return false;
		}
		return filter.match(System.getProperties());
	}

	/**
	 * Extension of Vector for attributes.
	 */
	static class Attribute extends Vector {
		private static final long serialVersionUID = 3257005440914174512L;

		/**
		 * Attribute constructor.
		 *  
		 */
		Attribute() {
			super(10, 10);
		}

		/**
		 * Perform an "OR" operation on equals.
		 * 
		 * @param obj
		 *            Object to test against.
		 * @return true if at least one attribute is equal; false otherwise.
		 */
		public synchronized boolean equals(Object obj) {
			for (int i = 0; i < elementCount; i++) {
				Object data = elementData[i];
				if (data instanceof String) {
					if (elementData[i].equals(obj)) {
						return (true);
					}
				} else {
					Enumeration e = ((Vector) data).elements();
					while (e.hasMoreElements()) {
						if (((String) e.nextElement()).equals(obj)) {
							return true;
						}
					}
				}
			}
			return (false);
		}

		/**
		 * Add the object if it is not already in the vector.
		 * 
		 * @param obj
		 *            Object to add to the vector.
		 */
		public synchronized void addElement(Object obj) {
			if (!contains(obj)) {
				super.addElement(obj);
			}
		}
	}
}
