/*
 * $Id: Manifest.java 84 2008-08-28 08:11:30Z peter.kriens@aqute.biz $
 * 
 * Copyright (c) OSGi Alliance (2002, 2006, 2007). All Rights Reserved.
 * 
 * Licensed 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.osgi.impl.bundle.obr.resource;

import java.io.*;
import java.util.*;


public class Manifest extends Hashtable {
	static final long	serialVersionUID	= 1L;
	List				imports;
	List				exports;
	ManifestEntry		name;
	String				activator;
	String				classpath[]	= new String[] {"."};
	int					section;
	String				location;
	Native				_native[];
	Vector				duplicates	= new Vector();
	final static String	wordparts	= "~!@#$%^&*_/?><.-+";
	ManifestEntry		bsn;
	VersionRange			version;
	ManifestEntry		host;
	List				require;

	public Manifest(InputStream in) throws IOException {
		parse(new InputStreamReader(in, "UTF8"));
	}

	public Manifest(Reader in) throws IOException {
		parse(in);
	}

	public Object put(Object header, Object value) {
		if (containsKey(header)) {
			if (!((String) header).equalsIgnoreCase("comment"))
				duplicates.add(header + ":" + value);
		}
		return super.put(header, value);
	}

	void parse(Reader in) throws IOException {
		BufferedReader rdr = new BufferedReader(in);
		String current = " ";
		String buffer = rdr.readLine();
		int section = 0;
		if (buffer != null && !buffer.startsWith("Manifest-Version")) {
			System.err
					.println("The first line of a manifest file must be the Manifest-Version attribute");
			throw new IOException(
					"The first line of a manifest file must be the Manifest-Version attribute");
		}
		while (buffer != null && current != null && section == 0) {
			if (current.startsWith(" ")) {
				buffer += current.substring(1);
			}
			else {
				section += entry(buffer);
				buffer = current;
			}
			current = rdr.readLine();
		}
		entry(buffer);
	}

	int entry(String line) throws IOException {
		if (line.length() < 2)
			return 1;
		int colon = line.indexOf(':');
		if (colon < 1) {
			error("Invalid header '" + line + "'");
		}
		else {
			String header = line.substring(0, colon).toLowerCase();
			String alphanum = "abcdefghijklmnopqrstuvwxyz0123456789";
			String set = alphanum;
			if (alphanum.indexOf(header.charAt(0)) < 0)
				error("Header does not start with alphanum: " + header);
			for (int i = 0; i < header.length(); i++) {
				if (set.indexOf(header.charAt(i)) < 0)
					error("Header contains non alphanum, - _: " + header);
				set = "_-" + alphanum;
			}
			String value = "";
			if (colon + 2 < line.length())
				value = line.substring(colon + 2);
			else
				error("No value for manifest header " + header);
			if (section == 0) {
				if (header.equals("bundle-symbolicname")) {
					bsn = (ManifestEntry) getEntries(value).get(0);
				}
				if (header.equals("bundle-version")) {
					try {
						version = new VersionRange(value.trim());
					}
					catch (Exception e) {
						version = new VersionRange("0");
						System.err.println("Invalid version attr for: " + bsn
								+ " value is " + value);
					}
				}
				if (header.equals("fragment-host"))
					host = (ManifestEntry) getEntries(value).get(0);
				if (header.equals("require-bundle"))
					require = getEntries(value);
				if (header.equals("import-package"))
					imports = getEntries(value);
				else if (header.equals("export-package"))
					exports = getEntries(value);
				else if (header.equals("bundle-activator"))
					activator = value.trim();
				else if (header.equals("bundle-updatelocation"))
					location = value.trim();
				else if (header.equals("bundle-classpath"))
					classpath = getClasspath(value);
				else if (header.equals("bundle-nativecode"))
					_native = getNative(value);
				put(header, value);
			}
		}
		return 0;
	}

	void error(String msg) throws IOException {
		System.err.println("Reading manifest: " + msg);
	}

	void warning(String msg) throws IOException {
		System.err.println("Reading manifest: " + msg);
	}

	StreamTokenizer getStreamTokenizer(String line) {
		StreamTokenizer st = new StreamTokenizer(new StringReader(line));
		st.resetSyntax();
		st.wordChars('a', 'z');
		st.wordChars('A', 'Z');
		st.wordChars('0', '9');
		st.whitespaceChars(0, ' ');
		st.quoteChar('"');
		for (int i = 0; i < wordparts.length(); i++)
			st.wordChars(wordparts.charAt(i), wordparts.charAt(i));
		return st;
	}

	String word(StreamTokenizer st) throws IOException {
		switch (st.nextToken()) {
			case '"' :
			case StreamTokenizer.TT_WORD :
				String result = st.sval;
				st.nextToken();
				return result;
		}
		return null;
	}

	Parameter getParameter(StreamTokenizer st) throws IOException {

		Parameter parameter = new Parameter();
		parameter.key = word(st);
		if (st.ttype == ':') {
			st.nextToken();
			parameter.type = Parameter.DIRECTIVE;
		}
		else {
			parameter.type = Parameter.ATTRIBUTE;
		}

		if (st.ttype == '=') {
			parameter.value = word(st);
			while (st.ttype == StreamTokenizer.TT_WORD || st.ttype == '"') {
				parameter.value += " " + st.sval;
				st.nextToken();
			}
		}

		return parameter;
	}

	public List getEntries(String line) throws IOException {
		List v = new Vector();
		Set aliases = new HashSet();

		StreamTokenizer st = getStreamTokenizer(line);
		do {
			Parameter parameter = getParameter(st);
			ManifestEntry p = new ManifestEntry(parameter.key);
			while (st.ttype == ';') {
				parameter = getParameter(st);
				if (parameter.value == null) {
					aliases.add(parameter.key);
				}
				else {
					if (parameter.type == Parameter.ATTRIBUTE)
						p.addParameter(parameter);
					else if ( parameter.type == Parameter.DIRECTIVE )
						p.addParameter(parameter);
					else
						p.addParameter(parameter);
				}
			}
			v.add(p);
			for (Iterator a = aliases.iterator(); a.hasNext();) {
				v.add(p.getAlias((String) a.next()));
			}
		} while (st.ttype == ',');
		return v;
	}

	Native[] getNative(String line) throws IOException {
		Vector v = new Vector();
		StreamTokenizer st = getStreamTokenizer(line);
		do {
			Native spec = new Native();
			Vector names = new Vector();
			do {
				Parameter parameter = getParameter(st);
				if (parameter.value == null)
					names.add(parameter.key);
				else if (parameter.is("processor", Parameter.ATTRIBUTE))
					spec.processor = parameter.value;
				else if (parameter.is("osname", Parameter.ATTRIBUTE))
					spec.osname = parameter.value;
				else if (parameter.is("osversion", Parameter.ATTRIBUTE))
					spec.osversion = parameter.value;
				else if (parameter.is("language", Parameter.ATTRIBUTE))
					spec.language = parameter.value;
				else if (parameter.is("selection-filter", Parameter.DIRECTIVE))
					spec.filter = parameter.value;
				else
					warning("Unknown parameter for native code : " + parameter);
			} while (st.ttype == ';');
			spec.paths = new String[names.size()];
			names.copyInto(spec.paths);
			v.add(spec);
		} while (st.ttype == ',');
		Native[] result = new Native[v.size()];
		v.copyInto(result);
		return result;
	}

	String[] getClasspath(String line) throws IOException {
		StringTokenizer st = new StringTokenizer(line, " \t,");
		String result[] = new String[st.countTokens()];
		for (int i = 0; i < result.length; i++)
			result[i] = st.nextToken();
		return result;
	}

	public List getImports() {
		return imports;
	}

	public List getExports() {
		return exports;
	}

	public String getActivator() {
		return activator;
	}

	public String getLocation() {
		return location;
	}

	public String[] getClasspath() {
		return classpath;
	}

	public Native[] getNative() {
		return _native;
	}

	public Object get(Object key) {
		if (key instanceof String)
			return super.get(((String) key).toLowerCase());
		else
			return null;
	}

	public String getValue(String key) {
		return (String) super.get(key.toLowerCase());
	}

	public String getValue(String key, String deflt) {
		String s = getValue(key);
		if (s == null)
			return deflt;
		else
			return s;
	}

	public String[] getRequiredExecutionEnvironments() {
		String ees = getValue("Bundle-RequiredExecutionEnvironment");
		if (ees != null)
			return ees.trim().split("\\s*,\\s*");
		else
			return null;
	}

	public VersionRange getVersion() {
		if (version == null)
			return new VersionRange("0");
		return version;
	}

	public String getSymbolicName() {
		ManifestEntry bsn = getBsn();

		if (bsn == null) {
			String name = getValue("Bundle-Name");
			if (name == null)
				name = "Untitled-" + hashCode();
			return name;
		}
		else
			return bsn.getName();
	}

	public String getManifestVersion() {
		return getValue("Bundle-ManifestVersion", "1");
	}

	public String getCopyright() {
		return getValue("Bundle-Copyright");
	}

	public String getDocumentation() {
		return getValue("Bundle-DocURL");
	}

	public String[] getCategories() {
		String cats = getValue("Bundle-Category");
		if (cats == null)
			return new String[0];
		else
			return cats.split("\\s*,\\s*");
	}

	public Native[] get_native() {
		return _native;
	}

	public void set_native(Native[] _native) {
		this._native = _native;
	}

	public ManifestEntry getBsn() {
		return bsn;
	}

	public void setBsn(ManifestEntry bsn) {
		this.bsn = bsn;
	}

	public Vector getDuplicates() {
		return duplicates;
	}

	public void setDuplicates(Vector duplicates) {
		this.duplicates = duplicates;
	}

	public ManifestEntry getHost() {
		return host;
	}

	public void setHost(ManifestEntry host) {
		this.host = host;
	}

	public List getRequire() {
		return require;
	}

}

class Native {
	String	filter;
	int		index	= -1;
	String	paths[];
	String	osname;
	String	osversion;
	String	language;
	String	processor;

}
