/*******************************************************************************
 * Copyright (c) 1997, 2009 by ProSyst Software GmbH
 * http://www.prosyst.com
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    ProSyst Software GmbH - initial API and implementation
 *******************************************************************************/
package org.eclipse.equinox.internal.ip.impl;

import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.eclipse.equinox.internal.ip.ProvisioningInfoProvider;
import org.eclipse.equinox.internal.ip.ProvisioningStorage;
import org.eclipse.equinox.internal.util.timer.Timer;
import org.eclipse.equinox.internal.util.timer.TimerListener;
import org.osgi.framework.*;
import org.osgi.service.provisioning.ProvisioningService;

/**
 * Class implementing provisioning management based on OSGi RFC 27. On start
 * this <I>BundleActivator</I> gets its manifest headers
 * <I>ProvisioningAgent.providerS</I> and <I>ProvisioningAgent.STORAGE</I>.
 * Using them it decides which <I>ProvisioningInfoProvider</I>-s and which
 * <I>ProvisioningStorage</I> are packed in the bundle. It make instances them.
 * To be succesfull instantiation they MUST have constructor without parameters.
 * All packed in bundle providers that implement <I>BundleActivator</I>. Their
 * methods <I>start(BundleContext)</I> are invoked after instantiation. (On
 * stop their <I>stop(BundleContext)</I>-s are invoked). ProvisioningStorage is
 * used for saving provisioning service data. If no such storage thatas are lost
 * on restart of bundle. Any fail in provider/storage instantiation, providers
 * casting to <I>BundleActivator</I> ends with exception in bundle start and
 * bundle will not reach state ACTIVE.
 * 
 * Also it starts DiscoveryAgent which is prosyst's feature. It is constructed
 * using values of system properties: "equinox.multicast.host" as host and
 * "equinox.multicast.port" as port. If one of these properties is null no
 * instance of DiscoveryAgent is made.
 * 
 * @author Avgustin Marinov
 * @author Pavlin Dobrev
 * @version 1.0
 */

public class ProvisioningAgent implements BundleActivator, ProvisioningService, ServiceListener, FrameworkListener, TimerListener {

	/**
	 * This manifest header determines the packed into the provisioning agent
	 * bundle storage (if such exists) that is to be setarted.
	 */
	public final static String STORAGE = "Prv-Storage";

	/**
	 * This manifest header determines URL Handlers packed into the provisioning
	 * agent bundle that are to be started.
	 */
	public final static String URL_HANDLERS = "Url-Handlers";

	/** This system property that determines multicast host for discoverer agent. */
	public final static String MULTICAST_HOST = "equinox.provisioning.multicast.host";

	/** This system property taht determines multicast port for discoverer agent. */
	public final static String MULTICAST_PORT = "equinox.provisioning.multicast.port";

	/**
	 * This system property determines if provisioning can use HTTP transport
	 * (assumed as unsecured) for provisioning data assignments.
	 */
	public final static String HTTP_ALLOWED = "equinox.provisioning.httpprv.allowed";

	/**
	 * This system property determines if provisioning must waits until the
	 * framework is started.
	 */
	public final static String WAIT_FW_START = "equinox.provisioning.prv.fwstart";

	/**
	 * This system property determines if provisioning should try to make
	 * provisioning on every start
	 */
	public final static String REPROVISIONING_ON_START = "equinox.provisioning.reprovision.onstart";

	/**
	 * Close Zip after reading.
	 */
	public final static String CLOSE_ZIP = "equinox.provisioning.close.zip";

	/**
	 * This system property determines if provisioning agent should print debug
	 * and error information on the console.
	 */
	public final static String DEBUG = "equinox.provisioning.debug";

	/**
	 * This system property determines if provisioning agent should print debug
	 * and error information on the console.
	 */
	public final static String REMOTE_DEBUG = "equinox.provisioning.remote.debug";

	/** BundleContext used for interactions with framework. */
	public static BundleContext bc;

	/** Provisioning data. */
	private ProvisioningData info;
	/** Registration of ProvisioningService. */
	private ServiceRegistration sreg;
	/**
	 * Configuration store used for data storing. It is null after start if no
	 * storage packed in bundle.
	 */
	private ProvisioningStorage storage;

	/** If HTTP protocol is allows for provisioning data assignments. */
	private boolean httpAllowed;

	/**
	 * If storage is inner packed but it is not inner provider, and is
	 * BundleActivator and its start method has been invoked, it must be
	 * stopped.
	 */
	private boolean destroyStorageOnStop = false;

	/**
	 * Contains providers packed in bundle. It can be null after start if no
	 * providers packed in bundle.
	 */
	private Vector providers;

	/**
	 * Contains URL handlers packed in bundle. It can be null after start if no
	 * providers packed in bundle.
	 */
	private Vector urlHandlers;

	/**
	 * DiscoveryAgent instantiated by this object on start(BundleContext). It is
	 * null after start id host or port arenot set in manifest.
	 */
	private Runnable da;

	/** If there is need to wait for start of framework. */
	private boolean wfs;
	/** If the information is added and provistioning assignements are allowed. */
	private boolean active;
	/**
	 * Flag if the start data is processed from frameworkEvent or from
	 * start(BundleContext) method.
	 */
	private boolean startProcessed;
	/** If to do reprovisiong */
	private boolean reprovision;
	/**
	 * If the service is already registered. Used to be avided duplicated
	 * registration on start.
	 */
	private boolean registered;
	/** If to close zip after reading */
	private boolean closeZip;

	// =================================================================================//
	private static final int PROVISIONING = 1;

	private static final String HAS_FAILED_PROVISIONG = "!@#$_hasFailedPrv";

	private boolean reAfterPrvFailureDisabled;
	private int a;
	private int b;
	private int changePeriod;
	private int maxPeriod;

	private Timer timer;
	private long nextProvisioningAfter;
	private int times;
	// =================================================================================//

	public final static int ERROR_UNKNOWN = 0;
	public final static int ERROR_LOAD_STORE_DATA = 1;
	public final static int ERROR_MALFORMED_URL = 2;
	public final static int ERROR_IO_EXCEPTION = 3;
	public final static int ERROR_CORRUPTED_ZIP = 4;

	/**
	 * Invoked by framework on bundle start.
	 * 
	 * @param bc
	 *            bundle context
	 * @exception java.lang.Exception
	 *                mostly when manifest dont match to packed
	 *                providers/storage or their implementation do not match
	 *                expectations.
	 */
	public void start(BundleContext bc) throws Exception {
		ProvisioningAgent.bc = bc;
		active = false;
		startProcessed = false;
		wfs = true;
		if (bc.getProperty(WAIT_FW_START) != null)
			if (bc.getProperty(WAIT_FW_START).equals("false"))
				wfs = false;
		httpAllowed = true;
		if (bc.getProperty(HTTP_ALLOWED) != null)
			if (bc.getProperty(HTTP_ALLOWED).equals("false"))
				httpAllowed = false;
		reprovision = getBoolean(ProvisioningAgent.REPROVISIONING_ON_START);
		closeZip = getBoolean(CLOSE_ZIP);

		// =================================================================================//
		reAfterPrvFailureDisabled = getBoolean("equinox.provisioning.provisioning.reAfterPrvFailure.disabled");
		a = getInteger("equinox.provisioning.provisioning.reAfterPrvFailure.a", 60000);
		b = getInteger("equinox.provisioning.provisioning.reAfterPrvFailure.b", 60000);
		changePeriod = getInteger("equinox.provisioning.provisioning.reAfterPrvFailure.changePeriod", 300000);
		maxPeriod = getInteger("equinox.provisioning.provisioning.reAfterPrvFailure.maxperiod", 3600000);
		nextProvisioningAfter = a;
		// =================================================================================//
		Log.j9workAround = getBoolean("equinox.provisioning.j9.2.0.workaround");
		Log.debug = getBoolean(DEBUG);
		Log.remoteDebug = getBoolean(REMOTE_DEBUG);
		Log.sendTrace = getBoolean("equinox.provisioning.send.trace");
		Log.prvSrv = this;

		org.eclipse.equinox.internal.util.ref.Log log = new org.eclipse.equinox.internal.util.ref.Log(bc);
		log.setDebug(true); // always log to LogService!
		log.setPrintOnConsole(Log.debug);
		Log.log = log;

		try {
			start0(bc);
		} catch (Exception exc) {
			Log.log.close();
			Log.log = null;
			throw exc;
		}
	}

	private void start0(BundleContext bc) throws Exception {
		Log.debug("Starting provisioning agent ...");

		Bundle thisBundle = bc.getBundle();

		// Starts URL handlers packed into the provisioning bundle
		String urlHandlersHeader = (String) thisBundle.getHeaders().get(URL_HANDLERS);
		if (urlHandlersHeader != null) {
			StringTokenizer strTok = new StringTokenizer(urlHandlersHeader, ", ");
			urlHandlers = new Vector(strTok.countTokens());
			while (strTok.hasMoreTokens()) {
				try {
					BundleActivator handler = (BundleActivator) Class.forName(strTok.nextToken().trim()).newInstance();
					handler.start(bc);
					urlHandlers.addElement(handler);
				} catch (Exception e) {
					Log.debug("Can't instantiate or start a handler!");
					throw e;
				}
			}
			urlHandlersHeader = null;
		}

		// Gets inner storage activator class name (if inner storage exists).
		String storageName = (String) thisBundle.getHeaders().get(STORAGE);
		if (storageName != null) {
			storageName = storageName.trim();
		}

		// Registers configuration providers packed into the bundle
		String providersHeader = (String) thisBundle.getHeaders().get(ProvisioningInfoProvider.PROVIDERS);
		if (providersHeader != null) {
			providers = new Vector(5);
			StringTokenizer strTok = new StringTokenizer(providersHeader, ",; ");
			while (strTok.hasMoreTokens()) {
				String providerName = strTok.nextToken().trim();
				strTok.nextToken(); // Skips ranking
				Object provider = Class.forName(providerName).newInstance();
				if (provider instanceof BundleActivator) {
					((BundleActivator) provider).start(bc);
				}
				if (providerName.equals(storageName)) {
					storage = (ProvisioningStorage) provider;
					destroyStorageOnStop = true;
				}
				providers.addElement(provider);
			}
		}

		if (storage == null && storageName != null && storageName.length() != 0) {
			try {
				storage = (ProvisioningStorage) Class.forName(storageName).newInstance();
				if (storage instanceof BundleActivator) {
					((BundleActivator) storage).start(bc);
					destroyStorageOnStop = true;
				}
			} catch (Exception e) {
				Log.debug("Can't instantiate or start storage \"" + storage + "\"!");
				throw e;
			}
		}

		info = new ProvisioningData();

		if (storage == null) {
			synchronized (this) {
				bc.addServiceListener(this, "(|" + '(' + Constants.OBJECTCLASS + '=' + ProvisioningStorage.class.getName() + ")" + '(' + Constants.OBJECTCLASS + '=' + ProvisioningInfoProvider.class.getName() + ")" + '(' + Constants.OBJECTCLASS + '=' + Timer.class.getName() + ')' + ")");

				storage = getStorage();
			}
		} else {
			bc.addServiceListener(this, "(|" + '(' + Constants.OBJECTCLASS + '=' + ProvisioningInfoProvider.class.getName() + ')' + '(' + Constants.OBJECTCLASS + '=' + Timer.class.getName() + ')' + ")");
		}

		synchronized (this) {
			if (timer == null) {
				ServiceReference sRef = bc.getServiceReference(Timer.class.getName());
				if (sRef != null) {
					timer = (Timer) bc.getService(sRef);
				}
			}
		}

		if (storage != null) {
			Log.debug("Loads from " + storage + " storage.");
			try {
				Dictionary storedInfo = storage.getStoredInfo();
				if (storedInfo != null && storedInfo.size() != 0) {
					info.add(storedInfo);
				}
			} catch (Exception e) { // NPE or storage specific exception can be
				// thrown
				Log.debug(e);
				setError(ERROR_LOAD_STORE_DATA, e.toString());
				if (destroyStorageOnStop) {
					Log.debug("Warning: the storage could be unavailable!");
				} else {
					storage = null;
				}
			}
		}

		if (getHasFailedPrv() && !reAfterPrvFailureDisabled) {
			reprovision = true;
		}

		boolean hasLoadedInfo = false;

		ServiceReference[] srefs = bc.getServiceReferences(ProvisioningInfoProvider.class.getName(), null);
		if (srefs != null) {
			sort(srefs);
			for (int i = 0; i < srefs.length; i++) {
				ProvisioningInfoProvider provider = ((ProvisioningInfoProvider) bc.getService(srefs[i]));
				try {
					synchronized (info) {
						if (!info.providers.contains(provider)) {
							Log.debug("Loads from " + provider + " provider.");
							info.providers.addElement(provider);
							Dictionary toAdd = provider.init(this);
							if (toAdd != null && toAdd.size() != 0) {
								String prvref = (String) toAdd.get(ProvisioningService.PROVISIONING_REFERENCE);
								if (prvref != null && prvref.trim().length() != 0) { // reference
									// is
									// changed
									// by
									// loader
									reprovision = true;
								}
								info.add(toAdd);
								hasLoadedInfo = true;
							}
						}
					}
				} catch (Exception e) {
					Log.debug(e);
				}
			}
		}

		if (!isFrameworkStarted()) {
			bc.addFrameworkListener(this);
		} else {
			wfs = false;
		}

		if (hasLoadedInfo) {
			store();
		}
		active = true;
		processStart();

		// Join GW to a multicast host.
		try {
			String host = "225.0.0.0";
			if (bc.getProperty(MULTICAST_HOST) != null)
				host = bc.getProperty(MULTICAST_HOST);
			String port = Integer.toString(getInteger(MULTICAST_PORT, 7777));
			if (host.length() != 0 && port.length() != 0) {
				Class dscAgentClass = Class.forName("org.eclipse.equinox.internal.ip.impl.dscagent.DiscoveryAgent");
				Constructor constr = dscAgentClass.getConstructor(new Class[] {String.class, int.class, BundleContext.class, ProvisioningAgent.class});
				new Thread(da = (Runnable) constr.newInstance(new Object[] {host, new Integer(Integer.parseInt(port)), bc, this}), "Discovery Agent").start();
			} // "info" is already initialized
		} catch (Throwable t) {
			Log.debug("Can't create discovery agent!");
		}
		Log.debug("Provisioning agent started ...");
	}

	/**
	 * @see org.osgi.framework.BundleActivator#stop(org.osgi.framework.BundleContext)
	 */
	public void stop(BundleContext bc) throws Exception {
		Log.debug("Stopping provisionig agent ...");

		if (timer != null) {
			try {
				timer.removeListener(this, PROVISIONING);
			} catch (Throwable _) {
			}
			timer = null;
		}

		registered = false;

		try {
			bc.removeServiceListener(this);
		} catch (Exception _) {
		}

		try {
			bc.removeFrameworkListener(this);
		} catch (Exception _) {
		}

		if (sreg != null) {
			try {
				sreg.unregister();
			} catch (Exception _) {
			}
			sreg = null;
		} // Unregisters ProvisioningService

		if (da != null) {
			try {
				Method close = da.getClass().getMethod("close", new Class[] {});
				close.invoke(da, new Object[0]);
			} catch (Exception e) {
				Log.debug(e);
			}
			da = null;
		} // Closes DiscoveryAgent

		if (storage != null) {
			if (destroyStorageOnStop) {
				try {
					if (storage instanceof BundleActivator) {
						((BundleActivator) storage).stop(bc);
					}
				} catch (Exception e) {
					Log.debug(e);
				}
				destroyStorageOnStop = false;
			}
			storage = null;
		} // Stores data

		if (providers != null) {
			for (int i = providers.size(); i-- > 0;) {
				try {
					((BundleActivator) providers.elementAt(i)).stop(bc);
				} catch (Exception e) {
					Log.debug(e);
				}
			}
			providers = null;
		} // Stops providers

		if (urlHandlers != null) {
			for (int i = urlHandlers.size(); i-- > 0;) {
				try {
					((BundleActivator) urlHandlers.elementAt(i)).stop(bc);
				} catch (Exception e) {
					Log.debug(e);
				}
			}
			urlHandlers = null;
		} // Stops providers

		info = null;
		Log.debug("Provisioning agent stopped ...");
		Log.log.close();
		Log.log = null;
	}

	public Dictionary getInformation() {
		return info;
	}

	public void setInformation(Dictionary info) {
		boolean refChanged = info.get(ProvisioningService.PROVISIONING_REFERENCE) != null;
		synchronized (this.info) {
			Integer version = (Integer) this.info.get(ProvisioningService.PROVISIONING_UPDATE_COUNT);
			Integer newVersion = new Integer(version.intValue() + 1);
			this.info.set(info);
			incrementUC(newVersion);
		}
		modify();
		updated(refChanged);
	}

	public void addInformation(Dictionary info) {
		addInformation(info, null);
	}

	private static final ByteArrayOutputStream baos = new ByteArrayOutputStream();
	private static final byte[] buffer = new byte[1024];

	private static byte[] readStream(InputStream is) throws IOException {
		synchronized (buffer) {
			baos.reset();
			int read;
			while ((read = is.read(buffer, 0, buffer.length)) != -1) {
				baos.write(buffer, 0, read);
			}
			return baos.toByteArray();
		}
	}

	private Bundle installBundle(String name, InputStream is) {
		Bundle bundle = null;
		try {
			bundle = getBundle(name);
			if (bundle == null) { /* install the bundle */
				if (Log.debug)
					Log.debug("Installing management bundle '" + name + "'");
				bundle = bc.installBundle(name, is);
			} else { /* just update it */
				if (Log.debug)
					Log.debug("Updating management bundle '" + name + "'");
				bundle.update(is);
			}
		} catch (Throwable t) {
			setHasFailedPrv(true);
			Log.debug("WARNING: Failed to install management bundle '" + name + "'", t);
		}
		return bundle;
	}

	public void addInformation(ZipInputStream zis) {
		Log.debug("Add Information form ZIS.");
		Hashtable entries = new Hashtable(2);//cache for unprocessed entries 
		boolean manifestFound = false;
		Dictionary info = new Hashtable(5);
		Dictionary entriesFromHeader = null;
		Dictionary extraFileds = null;
		Vector bundlesToStart = new Vector(5);
		String header = null;
		try {
			ZipEntry ze;
			String name, type;
			/* read the rest of the entries */
			while ((ze = zis.getNextEntry()) != null) {
				/* read name */
				name = ze.getName();
				if (name.endsWith("/")) {// path entry
					zis.closeEntry();
					continue;

				}
				if (name.charAt(0) == '/')
					name = name.substring(1);
				/* read extra */
				byte[] extra = ze.getExtra();
				type = extra == null ? null : new String(extra).toLowerCase();
				if (extra != null && !"META-INF/MANIFEST.MF".equals(name)) {
					if (extraFileds == null) {
						extraFileds = new Hashtable(3, 3);
					}
					extraFileds.put(name, type);
				}//the extra field is null or the entry is the manifest
				if (!manifestFound) {
					if ("META-INF/MANIFEST.MF".equals(name)) {//the entry is the manifest
						manifestFound = true;
						header = getHeaderFromManifest(zis);
						entriesFromHeader = filterAttributes(TYPE, parseEntries(header));
					} else {//no manifest yet, so cache the entry
						System.out.println("---put : " + name);
						entries.put(name, readStream(zis));
					}
				} else {//the manifest is found so we process the entry
					processEntry(extraFileds, name, null, zis, info, entriesFromHeader, bundlesToStart);
				}
				zis.closeEntry();
			}

			/*process the cached entries*/
			Enumeration names = entries.keys();
			while (names.hasMoreElements()) {
				processEntry(extraFileds, name = (String) names.nextElement(), (byte[]) entries.get(name), null, info, entriesFromHeader, bundlesToStart);
			}
		} catch (Throwable e) {
			this.info.setError(ERROR_CORRUPTED_ZIP, e.toString());
			Log.debug("Error reading provisioning package", e);
			setHasFailedPrv(true);
		}

		/* close the zip file */
		if (closeZip) {
			try {
				zis.close();
			} catch (Exception _) {
			}
		}

		/* update info and start all required bundles */
		addInformation(info, bundlesToStart); // bundle should
	}

	private void processEntry(Dictionary extraFileds, String name, byte[] content, InputStream is, Dictionary info, Dictionary entriesFromHeader, Vector bundlesToStart) throws IOException {
		/* 
		* first try the InitialProvisioning-Entries header
		* if the zip file had a manifest entry
		*/
		String type = entriesFromHeader == null ? null : (String) entriesFromHeader.get(name);
		/* 
		 * If there is no value in the InitialProvisioning-Entries header for that path
		 * try to initialize the type from the entry's extra field.
		 * If this ZIP entry field is present, the Initial Provisioning service should not
		 * look further, even if the extra field contains an erroneous value.
		 */
		if (type == null) {
			if (extraFileds != null) {
				type = (String) extraFileds.get(name);
			}
		}
		/*
		* if type is still null try to to initialize it 
		* according to the extension of the entry's name 
		*/
		if (type == null) {
			type = getMIMEfromExtension(name);
		}

		/* process entry */
		if (Log.debug) {
			Log.debug("Processing entry '" + name + "' of type " + type);
		}
		if (MIME_BUNDLE.equals(type) || MIME_BUNDLE_ALT.equals(type)) {
			installBundle(name, content == null ? new ISWrapper(is) : new ISWrapper(new ByteArrayInputStream(content)));
		} else if (MIME_BYTE_ARRAY.equals(type)) {
			info.put(name, content == null ? readStream(is) : content);
		} else if (MIME_STRING.equals(type)) {
			String value = getUTF8String(content == null ? readStream(is) : content);
			info.put(name, value);
			/*
			 * FIXME: actually there can be only ONE key of that type! - so why
			 * using vector
			 */
			if (PROVISIONING_START_BUNDLE.equals(name)) {
				/* Make management agent bundle deployment. Sets java.security.AllPermission
				 * if PermissionAdmin is available..*/
				try {
					grantAllPermissions(value);
				} catch (Throwable e) {
					Log.debug("Failed to grant all permissions", e);
				}
				bundlesToStart.addElement(value);
			}
		} else if (MIME_BUNDLE_URL.equals(type)) {
			String value = getUTF8String(content == null ? readStream(is) : content);
			installBundle(name, new URL(value).openStream());
		} else {
			this.info.setError(ERROR_CORRUPTED_ZIP, //
					"Unknown MIME type (" + type + ") for entry '" + name + "'");
			setHasFailedPrv(true);
		}
	}

	public void serviceChanged(ServiceEvent se) {
		Object service = bc.getService(se.getServiceReference());
		if (service instanceof ProvisioningStorage) {
			if (se.getType() == ServiceEvent.REGISTERED) {
				synchronized (this) {
					if (storage == null) { // If there is a storage it won't be
						// replaced.
						storage = (ProvisioningStorage) service;
					} else {
						return;
					}
				}
				try {
					Object oldUC = info.get(ProvisioningService.PROVISIONING_UPDATE_COUNT);
					int iOldUC = 0;
					if (oldUC != null && oldUC instanceof Integer) {
						iOldUC = ((Integer) oldUC).intValue();
					}

					Dictionary toAdd = storage.getStoredInfo();
					Object newUC = toAdd.get(ProvisioningService.PROVISIONING_UPDATE_COUNT);
					int iNewUC = 0;
					if (newUC != null && newUC instanceof Integer) {
						iNewUC = ((Integer) newUC).intValue();
					}

					boolean refChanged = toAdd.get(ProvisioningService.PROVISIONING_REFERENCE) != null && (iNewUC == 0 || reprovision);

					boolean increment = iNewUC > iOldUC; // In this case it
					// is assummed that
					// is most probably
					// that this
					// is the original version but storage is started after the
					// provisioning bundle.

					synchronized (info) {
						info.set(toAdd);
						if (increment) {
							incrementUC(new Integer(iNewUC));
							modify();
						}
					}

					updated(refChanged);
				} catch (Exception e) {
					Log.debug(e);
				}
				Log.debug("Storage is available.");
			} else if (se.getType() == ServiceEvent.UNREGISTERING) {
				if (storage == bc.getService(se.getServiceReference())) {
					Log.debug("Storage is removed!");
					try {
						storage.store(info);
					} catch (Exception e) {
						Log.debug("Can't store provisioning info!", e);
					}
					storage = null; // No more than one Storage service should
					// be registered on the FW
				}
			}
		}

		if (service instanceof ProvisioningInfoProvider) { // a Storage could
			// be a Provider
			if (se.getType() == ServiceEvent.REGISTERED) {
				ProvisioningInfoProvider provider = (ProvisioningInfoProvider) service;
				Log.debug("Loads from " + provider + " provider.");
				try {
					if (!info.providers.contains(provider)) {
						Dictionary toAdd = provider.init(this);
						boolean refChanged = false;
						if (toAdd != null) {
							String prvref = (String) toAdd.get(ProvisioningService.PROVISIONING_REFERENCE);
							if (prvref != null && prvref.trim().length() != 0) { // reference
								// is
								// changed
								// by
								// loader
								refChanged = true;
							}
						}
						boolean added = true;
						synchronized (info) {
							if (!info.providers.contains(provider)) {
								info.providers.addElement(provider);
								info.add(toAdd);
								if (refChanged) {
									reprovision = true;
								}
								Integer version = (Integer) info.get(ProvisioningService.PROVISIONING_UPDATE_COUNT);
								Integer newVersion = new Integer(version.intValue() + 1);
								incrementUC(newVersion);
							} else {
								added = false;
							}
						}
						if (added) {
							modify();
							updated(refChanged);
						}
					}
				} catch (Exception e) {
					Log.debug(e);
				}
			} else if (se.getType() == ServiceEvent.UNREGISTERING) {
				info.providers.removeElement(service);
			}
		}

		if (service instanceof Timer) { // timer is expected to be single
			// service
			switch (se.getType()) {
				case ServiceEvent.REGISTERED : {
					synchronized (this) {
						if (timer == null) {
							timer = (Timer) service;
							if (getHasFailedPrv() && !reAfterPrvFailureDisabled) {
								try {
									timer.notifyAfterMillis(this, nextPrvAfter(), PROVISIONING); // TO DO - Won't be
									// updated with newer
									// method - it will not
									// run on older FW
								} catch (Exception e) {
									Log.debug(e);
								}
							}
						}
					}
					break;
				}
				case ServiceEvent.UNREGISTERING : {
					timer = null;
					break;
				}
			}
		}
	}

	public synchronized void timer(int event) {
		Log.debug("Timer event " + event);
		try {
			switch (event) {
				case PROVISIONING : {
					Log.debug("Remake failed provisioning.");
					if (getHasFailedPrv()) {
						processPrvAssignment();
					}
					break;
				}
			}
		} catch (Exception e) {
			Log.debug(e);
		}
	}

	public void frameworkEvent(FrameworkEvent event) {
		if (event.getType() == FrameworkEvent.STARTED) {
			wfs = false;
			processStart();
		}
	}

	public boolean getHttpAllowed() {
		return httpAllowed;
	}

	// Synchronize frameworkEvent and start(BundleContext) initial reactions
	private void processStart() {
		synchronized (this) {
			try {
				Log.debug("Reprovision = " + reprovision + ", update counter = " + info.get(ProvisioningService.PROVISIONING_UPDATE_COUNT) + ", provisioning reference = " + info.get(ProvisioningService.PROVISIONING_REFERENCE));
				if ((reprovision || ((Integer) info.get(ProvisioningService.PROVISIONING_UPDATE_COUNT)).intValue() == 0) && info.get(ProvisioningService.PROVISIONING_REFERENCE) != null) {
					if (!startProcessed) {
						if (processPrvAssignment()) {
							startProcessed = true;
						}
					}
				}
			} catch (Throwable t) {
				Log.debug(t);
			}
		}

		synchronized (this) {
			if (active && !wfs && !registered) {
				registered = true;
			} else {
				return;
			}
		}
		Log.debug("Registering ProvisioningService.");
		sreg = bc.registerService(ProvisioningService.class.getName(), this, getRegProps());
	}

	/**
	 * This is called by ProvisioningData when one puts new
	 * <I>ProvisioningService.PROVISIONING_UPDATE_COUNT</I>.
	 * 
	 * @param refChanged
	 *            if ProvisioningService.PROVISIONING_REFERENCE changed
	 */
	private void updated(boolean refUpdated) {
		Log.debug("ProvisioingDictionary updated. Reference updated = " + refUpdated);

		store();

		synchronized (this) {
			if (refUpdated) {
				if (!processPrvAssignment()) {
					reprovision = true;
				}
			}
		}
	}

	private void store() {
		if (storage != null) {
			try {
				Dictionary info = this.info;
				if (info != null) {
					storage.store(info);
				}
			} catch (Exception e) {
				Log.debug(e);
			}
		} else {
			Log.debug("Warning: No storage available.");
		}
	}

	private boolean processPrvAssignment() {
		if (active && !wfs) {
			new Thread() {
				public void run() {
					try {
						process();
					} catch (Exception e) {
						Log.debug(e);
					}
				}
			}.start();
			return true;
		}
		return false;
	}

	/**
	 * Make provisioning data assignement to nextRef.
	 * 
	 * @param nextRef
	 *            next reference.
	 */
	synchronized void process() {
		if (info == null)
			return; // the bundle is stopped
		setHasFailedPrv(false);
		String ref = (String) info.get(ProvisioningService.PROVISIONING_REFERENCE);
		Log.debug("Reference = \"" + ref + '"');
		if (ref != null && (ref = ref.trim()).length() != 0) {
			String spid = (String) info.get(ProvisioningService.PROVISIONING_SPID);
			Log.debug("Setup from \"" + ref + "\", SPID = " + spid);

			if (!httpAllowed) {
				if (ref.startsWith("http://")) {
					Log.debug("Won't make setup to " + ref + " because http is forbidden!");
					setError(ERROR_MALFORMED_URL, "Provisioning reference is a HTTP URL, but non-secure HTTP is forbidden!");
					setHasFailedPrv(true);
					return;
				}
			}

			if (!ref.startsWith("file:") && ref.indexOf("service_platform_id") == -1) {
				if (ref.indexOf('?') == -1) {
					ref += '?';
				} else {
					ref += '&';
				}
				ref += "service_platform_id" + '=' + URLEncoder.encode(spid == null ? "" : spid);
			}
			Log.debug("Setup url = \"" + ref);

			URLConnection conn = null;

			try {
				URL url = new URL(ref);
				InputStream is = null;

				try {
					conn = url.openConnection();
					if (conn == null) {
						throw new IOException("Can't open connection to " + url + "!");
					}
					conn.setRequestProperty("Connection", "close");

					conn.connect();
					String error = conn.getHeaderField("error"); // Such
					// error
					// message
					// is not by
					// specification
					// and is
					// returned
					// by
					// Provisioining
					// Service
					// Backend
					if (error == null) {
						if ((conn instanceof HttpURLConnection) && ((HttpURLConnection) conn).getResponseCode() != HttpURLConnection.HTTP_OK) {
							String errorMsg = "Warning! ResponseCode = " + ((HttpURLConnection) conn).getResponseCode() + "!";
							Log.debug(errorMsg);
							setError(ERROR_IO_EXCEPTION, errorMsg);
						} else {
							is = conn.getInputStream();
						}
					} else {
						setError(ERROR_IO_EXCEPTION, error);
						Log.debug("Error from Backend: " + error + "! Setup failed!");
					}
				} catch (IOException e) {
					setError(ERROR_IO_EXCEPTION, e.toString());
					Log.debug(e);
				}

				if (is == null) {
					setHasFailedPrv(true);
				} else {
					try {
						ZipInputStream zis = new ZipInputStream(is);
						try {
							addInformation(zis); // the addInformation method
							// closes stream
							Log.debug("Setup ended.");
						} finally {
							if (!closeZip) {
								try {
									zis.close();
								} catch (Exception _) {
								}
							}
						}
					} catch (Exception e) {
						setError(ERROR_CORRUPTED_ZIP, e.toString());
						setHasFailedPrv(true);
						Log.debug(e);
					}
				}
			} catch (IOException e) { // new URL ---> MalformedURLException
				setError(ERROR_MALFORMED_URL, "Invalid Provisioning Reference: " + ref);
			} finally {
				if (conn != null && conn instanceof HttpURLConnection) {
					try {
						((HttpURLConnection) conn).disconnect();
					} catch (Exception _) {
					}
				}
			}
		}

		if (getHasFailedPrv()) {
			if (!reAfterPrvFailureDisabled) {
				try {
					if (timer != null) {
						timer.notifyAfterMillis(this, nextPrvAfter(), PROVISIONING); // TO DO - Won't be updated with
						// newer method - it will not
						// run on older FW
					}
				} catch (Exception e) {
					Log.debug(e);
				}
			}
		} else {
			times = 0;
			nextProvisioningAfter = a;
		}
	}

	private synchronized long nextPrvAfter() {
		times++;
		if (nextProvisioningAfter < maxPeriod && nextProvisioningAfter * times > changePeriod) {
			nextProvisioningAfter += b;
			times = 0;
		}
		Log.debug("Next bootstrap after " + nextProvisioningAfter + "ms");
		return nextProvisioningAfter;
	}

	// bundlesToStart is null on addInfo from provider/storage, and != null on
	// add from ZIS
	// pending error must be cleared only in second case!
	private void addInformation(Dictionary info, Vector bundlesToStart) {
		boolean refChanged = info.get(ProvisioningService.PROVISIONING_REFERENCE) != null;
		synchronized (this.info) {
			this.info.add(info);
			Integer version = (Integer) this.info.get(ProvisioningService.PROVISIONING_UPDATE_COUNT);
			Integer newVersion = new Integer(version.intValue() + 1);
			incrementUC(newVersion);
		}

		Log.debug("Bundles to start: " + bundlesToStart);
		if (bundlesToStart != null) {
			for (int i = 0; i < bundlesToStart.size(); i++) {
				Object next = bundlesToStart.elementAt(i);
				try {
					if (next instanceof Bundle) {
						((Bundle) next).start();
					} else {
						Bundle b = getBundle((String) next);
						if (b != null) {
							b.start();
						} else {
							Log.debug("Can't find '" + next + "' bundle to start it!");
						}
					}
				} catch (Exception e) {
					Log.debug("Exception while starting " + (next instanceof Bundle ? ((Bundle) next).getLocation() : next), e);
					setHasFailedPrv(true);
					return;
				}
			}

			clearError();
		}

		modify();
		updated(refChanged);
	}

	private boolean isFrameworkStarted() {
		if (!wfs)
			return true;
		Bundle system = bc.getBundle(0);
		return system != null ? system.getState() == Bundle.ACTIVE : true;
	}

	/**
	 * Make management agent bundle deployment. Sets java.security.AllPermission
	 * if PermissionAdmin is available..
	 * 
	 * @param maRef
	 *            management agent reference.
	 */
	private void grantAllPermissions(String location) {
		try {
			ServiceReference sref = bc.getServiceReference("org.osgi.service.permissionadmin.PermissionAdmin"); // the
			// org.osgi.service.permissionadmin is not imported for R1
			if (sref != null) {
				Method method = Class.forName("org.osgi.service.permissionadmin.PermissionAdmin").getMethod("setPermissions", new Class[] {String.class, Class.forName("[Lorg.osgi.service.permissionadmin.PermissionInfo;")});
				Object[] allPerms = (Object[]) Array.newInstance(Class.forName("org.osgi.service.permissionadmin.PermissionInfo"), 1);
				Constructor constr = Class.forName("org.osgi.service.permissionadmin.PermissionInfo").getConstructor(new Class[] {String.class, String.class, String.class});
				allPerms[0] = constr.newInstance(new String[] {"java.security.AllPermission", "", ""});
				method.invoke(bc.getService(sref), new Object[] {location, allPerms});
			}
		} catch (Exception e) {
			Log.debug(e);
		}
	}

	private ProvisioningStorage getStorage() {
		ServiceReference sref = bc.getServiceReference(ProvisioningStorage.class.getName());
		return sref != null ? (ProvisioningStorage) bc.getService(sref) : null;
	}

	private Bundle getBundle(String location) {
		Bundle[] bundles = bc.getBundles();
		for (int i = bundles.length; i-- > 0;) {
			if (location.equalsIgnoreCase(bundles[i].getLocation())) {
				return bundles[i];
			}
		}
		return null;
	}

	private void incrementUC(Integer uc) {
		if (!active) {
			return;
		}
		info.putUC(uc);
	}

	private void modify() {
		if (!active || sreg == null) {
			return;
		}
		try {
			sreg.setProperties(getRegProps());
		} catch (Exception e) {
		}
	}

	private Dictionary getRegProps() {
		Hashtable prvsprops = new Hashtable(1, 1.0F);
		prvsprops.put("Vendor", "ProSyst");
		prvsprops.put(ProvisioningService.PROVISIONING_UPDATE_COUNT, info.get(ProvisioningService.PROVISIONING_UPDATE_COUNT));
		return prvsprops;
	}

	private void sort(ServiceReference[] srefs) {
		for (int i = srefs.length - 1; i > 0; i--) {
			for (int j = 0; j < i; j++) {
				if (less(srefs[j + 1], srefs[j])) {
					ServiceReference temp = srefs[j];
					srefs[j] = srefs[j + 1];
					srefs[j + 1] = temp;
				}
			}
		}
	}

	private boolean less(ServiceReference sref1, ServiceReference sref2) {
		return getRanking(sref1) < getRanking(sref2);
	}

	private int getRanking(ServiceReference sref) {
		Integer ranking = (Integer) sref.getProperty(Constants.SERVICE_RANKING);
		return ranking == null ? 0 : ranking.intValue();
	}

	private String getUTF8String(byte[] body) {
		try {
			return new String(body, "UTF-8");
		} catch (Exception _0) {
			try {
				return new String(body, "UTF8"); // for personal java
				// problems
			} catch (Exception _1) {
				return new String(body);
			}
		}
	}

	private void setError(int code, String message) {
		ProvisioningData data = this.info;
		if (data == null) {
			return;
		}

		data.setError(code, message);

		Integer version = (Integer) data.get(ProvisioningService.PROVISIONING_UPDATE_COUNT);
		Integer newVersion = new Integer(version.intValue() + 1);
		info.putUC(newVersion);

		modify();
	}

	private void clearError() {
		ProvisioningData data = this.info;
		if (data == null) {
			return;
		}

		data.clearError();
	}

	private boolean getHasFailedPrv() {
		return "true".equals(info.get(HAS_FAILED_PROVISIONG));
	}

	private void setHasFailedPrv(boolean hasFailedPrv) {
		if (getHasFailedPrv() != hasFailedPrv) {
			info.putPrivate(HAS_FAILED_PROVISIONG, hasFailedPrv ? "true" : null);
			store();
		}
	}

	/**
	 * Parses the InitialProvisioning-Entries manifest header.
	 * @param header the contents of the InitialProvisioning-Entries manifest header, if any
	 * @return Dictionary which maps entry paths to Dictionary representing the attributes
	 * 		   or null, if no header is found.
	 */
	private static Dictionary parseEntries(String header) {
		Dictionary entries = null;
		Dictionary attributes = null;
		header = removeWhiteSpaces(header);
		if (header == null || header.length() == 0)
			return null;
		int begin = 0, end = 1, length = header.length();
		boolean quoted = false;
		String path, attribute, value;

		entry: while (end != -1 && begin < length - 1) {
			end = readToken(header, begin, false, false); //read the path
			if (end == -1)
				break; //end of header, or only path

			switch (header.charAt(end)) {
				case ';' ://end of path , read attribute
					if (begin == end) {
						end = readToken(header, end + 1, false, true);
						begin = end + 1;
						continue;
					}
					path = header.substring(begin, end);
					begin = end + 1;
					//read the attributes
					while (end != -1 && begin < length - 1) {
						end = readToken(header, begin, quoted, false);
						if (end == -1)
							break entry;
						if (header.charAt(end) != '=' || begin == end) {
							//invalid syntax
							end = readToken(header, begin, false, true); //read the attribute name
							begin = end + 1;
							continue entry;
						}
						attribute = header.substring(begin, end);
						if (header.charAt(begin) == '\"') {
							quoted = true;
							begin = end + 2;
						} else
							begin = end + 1;

						if (begin >= length - 1)
							break entry;

						end = readToken(header, begin, quoted, false); //read the attribute value
						if (end == -1) {
							if (quoted) {
								//invalid syntax
								end = readToken(header, begin, false, true);
								begin = end + 1;
								continue entry;
							}
							//end of header or last attribute-value
							value = header.substring(begin, length);
							if (attributes == null)
								attributes = new Hashtable(2, 3);
							attributes.put(attribute, value);
							begin = end + 1;
							break;
						}
						switch (header.charAt(end)) {
							case ',' : //end of parameters list
								value = header.substring(begin, end);
								if (attributes == null)
									attributes = new Hashtable(2, 3);
								attributes.put(attribute, value);
								if (entries == null)
									entries = new Hashtable(3, 3);
								entries.put(path, attributes);
								attributes = null;
								continue entry;
							case ';' : //another parameter
								value = header.substring(begin, end);
								if (attributes == null)
									attributes = new Hashtable(2, 3);
								attributes.put(attribute, value);
								begin = end + 1;
								continue;
							case '\"' : //endof quoted part
								quoted = false;
								value = header.substring(begin, end - 1);
								if (attributes == null)
									attributes = new Hashtable(2, 3);
								attributes.put(attribute, value);
								begin = end + 1;
								continue;
							default : //invalid syntax
								end = readToken(header, begin, false, true);
								begin = end + 1;
								continue entry;
						}
					}
					break;
				case ',' : //no description
					if (begin == end)
						begin = end + 2;
					else
						begin = end + 1;
					continue;
				default :// invalid syntax
					end = readToken(header, end + 1, false, true);
					begin = end + 1;
					continue;
			}

			if (attributes != null) {
				if (entries == null)
					entries = new Hashtable(3, 3);
				entries.put(path, attributes);
				attributes = null;
			}
		}
		return entries;
	}

	/**
	 * Reads the next token from the manifest header.
	 * @param token the valiue of the manifest header.
	 * @param begin index of the char from which to start
	 * @param quoted if the consequent part of teh string is guoted
	 * @param skipInvalidPath if true all chars till the first comma are skipped
	 * @return the index of the first character from the string which is 
	 *         different from the expected, or -1 if not found
	 */
	private static int readToken(String token, int begin, boolean quoted, boolean skipInvalidPath) {
		char c = 0;
		int len = token.length();
		if (begin >= len)
			return -1;
		if (quoted) {
			while ((c = token.charAt(begin)) != '\"') {
				if (++begin == len)
					return -1;
			}
			return begin;
		}
		if (skipInvalidPath) {
			while ((c = token.charAt(begin)) != ',') {
				if (++begin == len)
					return -1;
			}
			return begin;
		}
		while ((c = token.charAt(begin)) >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-' || c == '/' || c == '.' || c == ':') {
			if (++begin == len)
				return -1;
		}
		return begin;
	}

	static final String TYPE = "type";

	/**
	 * Filters the Dictionary returned from {@link #parseEntries(String)}.
	 * Returns Dictionary which maps entry paths to values of the attribute,
	 * which name is given as argument.
	 */
	private static Dictionary filterAttributes(String attribute, Dictionary entries) {
		if (entries == null)
			return null;
		Dictionary filtered = null;
		Enumeration paths = entries.keys();
		Dictionary attributes = null;
		String path = null, type = null, mime = null;
		while (paths.hasMoreElements()) {
			path = (String) paths.nextElement();
			attributes = (Dictionary) entries.get(path);
			if ((type = (String) attributes.get(attribute)) != null && (mime = typeToMIME(type)) != null) {
				if (filtered == null)
					filtered = new Hashtable(3, 3);
				filtered.put(path, mime);
			}
		}
		return filtered;
	}

	/**
	 * Maps type to an appropriate mime type constant
	 * @param type the value of the type attribute from the InitialProvisioning-Entries header
	 * @return the mime type for the string type or null if type is not a valid type
	 */
	private static String typeToMIME(String type) {
		if ("text".equals(type))
			return ProvisioningService.MIME_STRING;
		if ("binary".equals(type))
			return ProvisioningService.MIME_BYTE_ARRAY;
		if ("bundle".equals(type))
			return ProvisioningService.MIME_BUNDLE;
		if ("bundle-url".equals(type))
			return ProvisioningService.MIME_BUNDLE_URL;
		return null;
	}

	/** 
	 * @param filename the name of the zip entry 
	 * @return the MIME type of the entry according to its extension 
	 *         or null if the mime type cannot be defined
	 */
	static String getMIMEfromExtension(String filename) {
		int index = filename.lastIndexOf(".");
		//no extension -> we cannot identify the type
		if (index == -1)
			return null;
		String extension = filename.substring(index + 1);
		if (extension.equals("jar"))
			return ProvisioningService.MIME_BUNDLE;
		if (extension.equals("txt"))
			return ProvisioningService.MIME_STRING;
		if (extension.equals("url"))
			return ProvisioningService.MIME_BUNDLE_URL;
		return ProvisioningService.MIME_BYTE_ARRAY;
	}

	/**
	 * @param is the InputStream containing the information in the manifest
	 * @return the contents of the InitialProvisioning-Entries header if any or null otherwise 
	 */
	private static String getHeaderFromManifest(InputStream is) {
		boolean blank = false;
		StringBuffer header = new StringBuffer();
		BufferedReader br = new BufferedReader(new InputStreamReader(is));
		String line = null;
		boolean loop = true;
		try {
			while ((line = br.readLine()) != null && loop) {
				if (line.length() == 0) {
					if (blank) {
						break;
					}
					blank = true;
					continue;
				} else {
					blank = false;
				}
				if (line.startsWith(ProvisioningService.INITIALPROVISIONING_ENTRIES)) {
					header.append(removeWhiteSpaces(line.substring(ProvisioningService.INITIALPROVISIONING_ENTRIES.length() + 1)));
					line = br.readLine();//next line
					while (loop = (line.length() != 0 && Character.isWhitespace(line.charAt(0)))) {
						header.append(removeWhiteSpaces(line));
						line = br.readLine();
					}
				}
			}
		} catch (IOException ioe) {
			return null;
		}
		return (header.length() == 0 ? null : header.toString());
	}

	private static String removeWhiteSpaces(String s) {
		if (s == null)
			return null;
		char curr;
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < s.length(); i++) {
			if (!Character.isWhitespace(curr = s.charAt(i))) {
				sb.append(curr);
			}
		}
		return sb.toString();
	}

	private static class ISWrapper extends InputStream {

		private InputStream is;

		ISWrapper(InputStream is) {
			this.is = is;
		}

		public int read() throws IOException {
			return is.read();
		}

		public int read(byte[] src, int off, int len) throws IOException {
			return is.read(src, off, len);
		}

		public void close() {
		}
	}

	public static boolean getBoolean(String property) {
		String prop = (bc != null) ? bc.getProperty(property) : System.getProperty(property);
		return ((prop != null) && prop.equalsIgnoreCase("true"));
	}

	public static int getInteger(String property, int defaultValue) {
		String prop = (bc != null) ? bc.getProperty(property) : System.getProperty(property);
		if (prop != null) {
			try {
				return Integer.decode(prop).intValue();
			} catch (NumberFormatException e) {
				//do nothing
			}
		}
		return defaultValue;
	}
}
