/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2012 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.loader;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.util.tracker.BundleTrackerCustomizer;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;

/**
 * Tracker of META/INF entries in bundles.
 * @author Florent Benoit
 */
public class MetaInfScannerBundleTrackerCustomizer implements BundleTrackerCustomizer {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(MetaInfScannerBundleTrackerCustomizer.class);

    /**
     * Filter : we won't keep entries starting with these names.
     */
    private static final String[] DENY_FILTERS = new String[] {"META-INF/maven", "META-INF/MANIFEST.MF", "META-INF/LICENSE"};

    /**
     * Associate a bundle and a list of META/INF URL entries for the given
     * bundle.
     */
    private Map<Bundle, List<URL>> bundleMapEntries = null;

    /**
     * Associate a entry name and the list of all URL resources that are
     * providing these entries.
     */
    private Map<String, List<URL>> urlEntries = null;

    /**
     * Default constructor.
     * @param bundleContext the given bundle context
     */
    public MetaInfScannerBundleTrackerCustomizer(final BundleContext bundleContext) {
        this.bundleMapEntries = new WeakHashMap<Bundle, List<URL>>();
        this.urlEntries = new HashMap<String, List<URL>>();
    }

    /**
     * A bundle is being added to the BundleTracker. This method is called
     * before a bundle which matched the search parameters of the BundleTracker
     * is added to the BundleTracker. This method should return the object to be
     * tracked for the specified Bundle. The returned object is stored in the
     * BundleTracker and is available from the getObject method.
     * @param bundle The Bundle being added to the BundleTracker.
     * @param event The bundle event which caused this customizer method to be
     * called or null if there is no bundle event associated with the call to
     * this method.
     * @return The object to be tracked for the specified Bundle object or null
     * if the specified Bundle object should not be tracked.
     */
    @Override
    public Object addingBundle(final Bundle bundle, final BundleEvent event) {
        logger.debug("Bundle ''{0}'' with event ''{1}''", bundle, event);
        return register(bundle);
    }

    /**
     * A bundle tracked by the BundleTracker has been modified. This method is
     * called when a bundle being tracked by the BundleTracker has had its state
     * modified.
     * @param bundle The Bundle whose state has been modified.
     * @param event The bundle event which caused this customizer method to be
     * called or null if there is no bundle event associated with the call to
     * this method.
     * @param object The tracked object for the specified bundle.
     */
    @Override
    public void modifiedBundle(final Bundle bundle, final BundleEvent event, final Object object) {
        logger.debug("Bundle ''{0}'' with event ''{1}''", bundle, event);
        if (bundle.getState() == Bundle.STOPPING || bundle.getState() == Bundle.ACTIVE) {
            unregister(bundle);
        }
        if (bundle.getState() == Bundle.ACTIVE) {
            register(bundle);
        }

    }

    /**
     * A bundle tracked by the BundleTracker has been removed. This method is
     * called after a bundle is no longer being tracked by the BundleTracker.
     * @param bundle The Bundle that has been removed.
     * @param event The bundle event which caused this customizer method to be
     * called or null if there is no bundle event associated with the call to
     * this method.
     * @param object The tracked object for the specified bundle.
     */
    @Override
    public void removedBundle(final Bundle bundle, final BundleEvent event, final Object object) {
        logger.debug("Bundle ''{0}'' with event ''{1}''", bundle, event);
        unregister(bundle);
    }

    /**
     * Find the URLs entries available in the given Bundle.
     * @param bundle the bundle to analyze
     * @return the updated list of URLs found in META-INF directory
     */
    public List<URL> register(final Bundle bundle) {

        List<URL> list = new ArrayList<URL>();

        // Scan all entries and search for META-INF entries
        Enumeration<URL> enumeration = bundle.findEntries("META-INF", "*", true);
        if (enumeration != null) {
            while (enumeration.hasMoreElements()) {
                URL url = enumeration.nextElement();
                String path = url.getPath();

                if (path.length() > 1) {
                    path = path.substring(1);
                }

                // directory, do not check
                if (path.endsWith("/")) {
                    continue;
                }

                // Filter
                boolean denied = false;
                for (String filter : DENY_FILTERS) {
                    if (path.startsWith(filter)) {
                        denied = true;
                        break;
                    }
                }
                if (denied) {
                    continue;
                }

                list.add(url);
            }
            synchronized (this.bundleMapEntries) {
                this.bundleMapEntries.put(bundle, list);
            }
            refresh();
        }
        return list;

    }

    /**
     * Unregister the URLs associated to the given bundle.
     * @param bundle the bundle that is stopping
     */
    public void unregister(final Bundle bundle) {
        // remove all the urls associated to this bundle
        synchronized (this.bundleMapEntries) {
            this.bundleMapEntries.remove(bundle);
        }
        refresh();

    }

    /**
     * Refresh data of all URL entries.
     */
    public void refresh() {
        synchronized (this.bundleMapEntries) {

            synchronized (this.urlEntries) {
                // Reset current state
                this.urlEntries.clear();

                // Compute list of entries
                Collection<List<URL>> listUrls = this.bundleMapEntries.values();
                Iterator<List<URL>> itListURLS = listUrls.iterator();
                while (itListURLS.hasNext()) {
                    List<URL> urls = itListURLS.next();
                    for (URL url : urls) {

                        String path = url.getPath();

                        // Remove start / of all entries in order to get the name
                        if (path.length() > 1) {
                            path = path.substring(1);
                        }

                        List<URL> lstURL = this.urlEntries.get(path);
                        if (lstURL == null) {
                            lstURL = new ArrayList<URL>();
                            this.urlEntries.put(path, lstURL);
                        }
                        lstURL.add(url);
                    }
                }
            }
        }

    }

    /**
     * Gets the list of the current URL entries.
     * @param name the given name
     * @return the list of URL
     */
    public List<URL> getEntries(final String name) {
        // Return a clone
        List<URL> newList = null;
        synchronized (this.urlEntries) {
            List<URL> foundList = this.urlEntries.get(name);
            if (foundList != null) {
                newList = new ArrayList<URL>();
                newList.addAll(foundList);
            }
        }
        return newList;
    }
}
