com.alexkli.osgi.troubleshoot.impl.TroubleshootServlet.java Source code

Java tutorial

Introduction

Here is the source code for com.alexkli.osgi.troubleshoot.impl.TroubleshootServlet.java

Source

/**************************************************************************
 * 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 com.alexkli.osgi.troubleshoot.impl;

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.felix.webconsole.SimpleWebConsolePlugin;
import org.apache.felix.webconsole.WebConsoleConstants;
import org.apache.felix.webconsole.WebConsoleUtil;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.runtime.ServiceComponentRuntime;
import org.osgi.service.component.runtime.dto.ComponentConfigurationDTO;
import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;
import org.osgi.service.component.runtime.dto.ReferenceDTO;
import org.osgi.service.component.runtime.dto.SatisfiedReferenceDTO;
import org.osgi.service.packageadmin.ExportedPackage;
import org.osgi.service.packageadmin.PackageAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alexkli.osgi.troubleshoot.impl.utils.Clause;
import com.alexkli.osgi.troubleshoot.impl.utils.Parser;

/**
 * Web console view that helps troubleshooting unresolved bundles and co.
 */
@Component(immediate = true)
@Service(value = { Servlet.class })
@Properties({ @Property(name = Constants.SERVICE_DESCRIPTION, value = "Web Console OSGi Troubleshoot Plugin"),
        @Property(name = WebConsoleConstants.PLUGIN_LABEL, value = TroubleshootServlet.LABEL),
        @Property(name = WebConsoleConstants.PLUGIN_TITLE, value = TroubleshootServlet.TITLE),
        @Property(name = WebConsoleConstants.PLUGIN_CATEGORY, value = TroubleshootServlet.CATEGORY) })
@SuppressWarnings("serial")
public class TroubleshootServlet extends SimpleWebConsolePlugin {

    public static final String LABEL = "troubleshoot";
    public static final String TITLE = "Troubleshoot";
    public static final String CATEGORY = "OSGi";

    private final Logger log = LoggerFactory.getLogger(getClass());

    @Reference
    private PackageAdmin packageAdmin;

    @Reference
    private ServiceComponentRuntime scr;

    private ServiceOriginTracker serviceOriginTracker;

    public TroubleshootServlet() {
        super(LABEL, TITLE, CATEGORY, null);
    }

    @Activate
    public void componentActivate(ComponentContext ctx) {
        BundleContext bundleContext = ctx.getBundleContext();
        activate(bundleContext);
        serviceOriginTracker = new ServiceOriginTracker(bundleContext);
    }

    @Deactivate
    public void componentDeactivate() {
        serviceOriginTracker.stop(getBundleContext());
        serviceOriginTracker = null;

        deactivate();
    }

    // ----------------------------------------------< main view >---------------------------------

    @Override
    protected void renderContent(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {
        PrintWriter out = res.getWriter();

        embedStyle(out, "css/troubleshoot.css");
        embedScript(out, "js/troubleshoot.js");

        final Bundle[] bundles = getBundleContext().getBundles();

        handleBundles(req, res, bundles);

        handleServices(req, res);
    }

    // ----------------------------------------------< actions >---------------------------------

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        final String action = WebConsoleUtil.getParameter(request, "action");
        if ("startInactiveBundles".equals(action)) {
            startActionResponse(request, response);

            PrintWriter out = response.getWriter();
            int bundlesTouched = 0;
            int bundlesActive = 0;

            final Bundle[] bundles = getBundleContext().getBundles();
            for (Bundle bundle : bundles) {
                if (isFragmentBundle(bundle)) {
                    continue;
                }
                if (bundle.getState() == Bundle.RESOLVED || bundle.getState() == Bundle.INSTALLED) {
                    bundlesTouched++;

                    try {
                        out.printf("Trying to start %s (%s)... ", bundle.getSymbolicName(),
                                getStatusString(bundle));
                        response.flushBuffer();

                        bundle.start(Bundle.START_TRANSIENT);

                        bundlesActive += 1;

                        out.printf("<span class='log-ok'>OK: %s.</span>", getStatusString(bundle));

                    } catch (BundleException e) {
                        out.printf("<span class='ui-state-error-text'>Failed:</span> %s", e.getMessage());
                    } catch (IllegalStateException e) {
                        out.printf("<span class='ui-state-error-text'>Failed, state changed:</span> %s",
                                e.getMessage());
                    } catch (SecurityException e) {
                        out.printf("<span class='ui-state-error-text'>Denied:</span> %s", e.getMessage());
                    }

                    out.println("<br/>");
                    insertScrollScript(out);
                    response.flushBuffer();
                }
            }

            out.println("<br/>");
            if (bundlesTouched == 0) {
                out.println("<span class='log-end'>No installed or resolved bundles found</span><br/>");
            } else {
                out.printf("<span class='log-end'>Successfully started %s out of %s bundles.</span><br/>",
                        bundlesActive, bundlesTouched);
            }

            insertScrollScript(out);
            endActionResponse(response);
        }
    }

    private void startActionResponse(HttpServletRequest request, HttpServletResponse response) throws IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");

        PrintWriter out = response.getWriter();
        out.println("<head>");
        String appRoot = (String) request.getAttribute(WebConsoleConstants.ATTR_APP_ROOT);
        includeCSS(out, appRoot + "/res/lib/reset-min.css");
        includeCSS(out, appRoot + "/res/lib/themes/base/jquery-ui.css");
        includeCSS(out, appRoot + getBrandingPlugin().getMainStyleSheet());
        embedStyle(out, "css/action.css");
        embedScript(out, "js/action.js");
        out.println("</head>");

        out.println("<body class='ui-widget'>");

        // add padding to force immediate flushing (response.flushBuffer() alone isn't enough at the start)
        for (int i = 0; i < 100; i++) {
            out.print(" ");
        }
    }

    private void endActionResponse(HttpServletResponse response) throws IOException {
        PrintWriter out = response.getWriter();
        out.println("</body>");
    }

    private void insertScrollScript(PrintWriter out) {
        embedScript(out, "js/action-scroll.js");
    }

    // ----------------------------------------------< bundles >---------------------------------

    private void handleBundles(HttpServletRequest request, HttpServletResponse response, Bundle[] bundles)
            throws IOException {
        PrintWriter out = response.getWriter();

        out.println("<h2>Bundles</h2>");

        out.println("<p class='statline ui-state-highlight'>");
        out.println(getBundleStatusLine(bundles));
        out.println("</p>");

        final List<Bundle> problematicBundles = getProblematicBundles(bundles);

        if (problematicBundles.isEmpty()) {
            out.println("<div class='all-ok'>All bundles ok.</div>");
            return;
        }

        // button + dialog for starting all bundles
        out.println("<form class='startInactiveBundles' method='post' target='actionLog'>");
        out.println("    <input type='hidden' name='action' value='startInactiveBundles' />");
        out.println("    <button type='submit'>Start inactive bundles</button>");
        out.println("</form>");
        out.println("<div id='actionLogDialog' style='display:none'>");
        out.println(
                "   <iframe id='actionLog' name='actionLog' width='100%' height='100%' frameborder='0' marginwidth='0' marginheight='0'></iframe>");
        out.println("</div>");

        out.println("<div>");

        final String bundlesUrl = request.getAttribute(WebConsoleConstants.ATTR_APP_ROOT) + "/bundles";

        for (Bundle bundle : problematicBundles) {
            out.println(getDetailLink(bundle, bundlesUrl));
            out.println(" ");
            out.println(getStatusString(bundle));
            out.println("<br>");

            if (bundle.getState() == Bundle.STOPPING || bundle.getState() == Bundle.STARTING) {
                out.print("<span class='hint'>If the bundle is ");
                out.print(bundle.getState() == Bundle.STOPPING ? "stopping" : "starting");
                out.println(" forever, there might be a deadlock."
                        + " Check the <a href='status-jstack-threaddump'>thread dumps</a>.</span><br/>");
            }

            ExportedPackage[] allExports = packageAdmin.getExportedPackages((Bundle) null);
            // multimap - same package can be exported in multiple versions
            Map<String, List<ExportedPackage>> globalExportMap = new HashMap<String, List<ExportedPackage>>();
            for (int j = 0; j < allExports.length; j++) {
                ExportedPackage exportedPackage = allExports[j];
                List<ExportedPackage> values = globalExportMap.get(exportedPackage.getName());
                if (values == null) {
                    values = new ArrayList<ExportedPackage>();
                    globalExportMap.put(exportedPackage.getName(), values);
                }
                values.add(exportedPackage);
            }

            Dictionary dict = bundle.getHeaders();

            // go through imports
            // - other bundle might not be resolved
            // - something else exports it, but in another (older) version
            // - nothing exports it

            String importHeader = (String) dict.get(Constants.IMPORT_PACKAGE);
            Clause[] imports = Parser.parseHeader(importHeader);
            if (imports != null) {

                for (Clause importPkg : imports) {
                    if (isOptional(importPkg)) {
                        continue;
                    }
                    if (isOwnPackage(bundle, importPkg.getName())) {
                        continue;
                    }

                    String name = importPkg.getName();
                    List<ExportedPackage> matchingExports = globalExportMap.get(name);
                    if (matchingExports != null) {
                        boolean satisfied = false;
                        for (ExportedPackage exported : matchingExports) {
                            if (isSatisfied(importPkg, exported)) {
                                satisfied = true;

                                Bundle exportingBundle = exported.getExportingBundle();
                                if (isInactive(exportingBundle)) {
                                    // not an actual issue, just a chain of dependencies not resolving
                                    out.print("- dependency not active: ");
                                    out.print(getDetailLink(exportingBundle, bundlesUrl));
                                    out.print(" ");
                                    out.println(getStatusString(exportingBundle));
                                    out.print(" (importing ");
                                    out.print(name);
                                    out.print(")");
                                    out.println("<br>");
                                }
                                break;
                            }
                        }
                        if (!satisfied) {
                            // here we have export candidates, but in a different version
                            out.print("<span class='ui-state-error-text'>");

                            String prefix = "";
                            if (matchingExports.size() > 1) {
                                prefix = "candidate ";
                            }
                            // common case
                            if (matchingExports.size() == 1) {
                                for (ExportedPackage export : matchingExports) {
                                    Version exportVersion = export.getVersion();
                                    String versionAttr = importPkg.getAttribute(Constants.VERSION_ATTRIBUTE);
                                    VersionRange importRange = new VersionRange(versionAttr);

                                    out.print("- ");
                                    out.print(prefix);
                                    if ((importRange.getLeftType() == VersionRange.LEFT_CLOSED
                                            && exportVersion.compareTo(importRange.getLeft()) < 0)
                                            || (importRange.getLeftType() == VersionRange.LEFT_OPEN
                                                    && exportVersion.compareTo(importRange.getLeft()) <= 0)) {
                                        out.print("dependency too old: ");
                                    } else if ((importRange.getRightType() == VersionRange.RIGHT_CLOSED
                                            && exportVersion.compareTo(importRange.getRight()) > 0)
                                            || (importRange.getRightType() == VersionRange.RIGHT_OPEN
                                                    && exportVersion.compareTo(importRange.getRight()) >= 0)) {
                                        out.print("dependency too new: ");
                                    } else {
                                        out.print("dependency with different version: ");
                                    }

                                    out.print(getDetailLink(export.getExportingBundle(), bundlesUrl));
                                    out.print(" (importing ");
                                    out.print(name);
                                    out.print(" ");
                                    out.print(versionAttr);
                                    out.print(" but found ");
                                    out.print(exportVersion.toString());
                                    out.print(")");
                                }
                            }
                            out.print("</span>");
                            out.println("<br>");
                        }
                    } else {
                        // not found at all, bundle missing
                        out.print("<span class='ui-state-error-text'>");
                        out.print("- not exported by any bundle: ");
                        out.println(name);
                        out.print("</span>");
                        out.println("<br>");
                    }
                }
            }
            out.println("<br>");
        }
        out.println("</div>");
    }

    /** Check if this bundle exports this package */
    private boolean isOwnPackage(Bundle bundle, String packageName) {
        String path = packageName.replace('.', '/');
        return bundle.getEntry(path) != null;
    }

    private String getDetailLink(Bundle bundle, String bundlesUrl) {
        return "<a href='" + bundlesUrl + '/' + bundle.getBundleId() + "'>" + bundle.getSymbolicName() + " ("
                + bundle.getBundleId() + ")" + "</a>";
    }

    private boolean isSatisfied(Clause imported, ExportedPackage exported) {
        if (imported.getName().equals(exported.getName())) {
            String versionAttr = imported.getAttribute(Constants.VERSION_ATTRIBUTE);
            if (versionAttr == null) {
                // no specific version required, this export surely satisfies it
                return true;
            }

            VersionRange required = new VersionRange(versionAttr);
            return required.includes(exported.getVersion());
        }

        // no this export does not satisfy the import
        return false;
    }

    private boolean isOptional(Clause clause) {
        String directive = clause.getDirective(Constants.RESOLUTION_DIRECTIVE);
        return Constants.RESOLUTION_OPTIONAL.equals(directive);
    }

    private List<Bundle> getProblematicBundles(Bundle[] bundles) {
        List<Bundle> problemBundles = new ArrayList<Bundle>();
        for (Bundle bundle : bundles) {
            if (isInactive(bundle)) {
                problemBundles.add(bundle);
            }
        }
        return problemBundles;
    }

    private boolean isInactive(Bundle bundle) {
        if (isFragmentBundle(bundle)) {
            return bundle.getState() != Bundle.RESOLVED;
        } else {
            return bundle.getState() != Bundle.ACTIVE;
        }
    }

    private boolean isFragmentBundle(Bundle bundle) {
        // Workaround for FELIX-3670
        if (bundle.getState() == Bundle.UNINSTALLED) {
            return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
        }

        return getPackageAdmin().getBundleType(bundle) == PackageAdmin.BUNDLE_TYPE_FRAGMENT;
    }

    private String getStatusString(final Bundle bundle) {
        switch (bundle.getState()) {
        case Bundle.INSTALLED:
            return "Installed";
        case Bundle.RESOLVED:
            if (isFragmentBundle(bundle)) {
                return "Fragment";
            }
            return "Resolved";
        case Bundle.STARTING:
            return "Starting";
        case Bundle.ACTIVE:
            return "Active";
        case Bundle.STOPPING:
            return "Stopping";
        case Bundle.UNINSTALLED:
            return "Uninstalled";
        default:
            return "Unknown: " + bundle.getState();
        }
    }

    private String getBundleStatusLine(final Bundle[] bundles) {
        int active = 0, installed = 0, resolved = 0, fragments = 0;
        for (Bundle bundle : bundles) {
            switch (bundle.getState()) {
            case Bundle.ACTIVE:
                active++;
                break;
            case Bundle.INSTALLED:
                installed++;
                break;
            case Bundle.RESOLVED:
                if (isFragmentBundle(bundle)) {
                    fragments++;
                } else {
                    resolved++;
                }
                break;
            }
        }
        final StringBuffer buffer = new StringBuffer();
        buffer.append("Bundle information: ");
        appendBundleInfoCount(buffer, "in total", bundles.length);
        if (active == bundles.length || active + fragments == bundles.length) {
            buffer.append(" - all ");
            appendBundleInfoCount(buffer, "active.", bundles.length);
        } else {
            if (active != 0) {
                buffer.append(", ");
                appendBundleInfoCount(buffer, "active", active);
            }
            if (fragments != 0) {
                buffer.append(", ");
                appendBundleInfoCount(buffer, "active fragments", fragments);
            }
            if (resolved != 0) {
                buffer.append(", <span class='ui-state-error-text'>");
                appendBundleInfoCount(buffer, "resolved", resolved);
                buffer.append("</span>");
            }
            if (installed != 0) {
                buffer.append(", <span class='ui-state-error-text'>");
                appendBundleInfoCount(buffer, "installed", installed);
                buffer.append("</span>");
            }
            buffer.append('.');
        }
        return buffer.toString();
    }

    private void appendBundleInfoCount(final StringBuffer buf, String msg, int count) {
        buf.append(count);
        buf.append(" bundle");
        if (count != 1)
            buf.append('s');
        buf.append(' ');
        buf.append(msg);
    }

    private PackageAdmin getPackageAdmin() {
        return packageAdmin;
    }

    // ----------------------------------------------< services / components >---------------------------------

    private void handleServices(HttpServletRequest req, HttpServletResponse res) throws IOException {
        PrintWriter out = res.getWriter();

        final Collection<ComponentDescriptionDTO> allComponents = scr.getComponentDescriptionDTOs();

        ServiceReference<?>[] allServiceReferences = null;
        try {
            allServiceReferences = getBundleContext().getAllServiceReferences(null, null);
        } catch (InvalidSyntaxException ignore) {
            // filter is null
        }

        out.println("<h2>Components</h2>");
        out.println("<p class='statline ui-state-highlight'>");
        out.println(getServiceStatusLine(allComponents, allServiceReferences));
        out.println("</p>");

        out.println("<div>");

        // service interface name -> components implementing it
        Map<String, List<ComponentDescriptionDTO>> serviceImpls = getAvailableServiceImplementations(allComponents);
        Map<String, List<ComponentDescriptionDTO>> componentsByName = getAvailableComponentsByName(allComponents);

        // service interface name -> components blocked by it
        Map<String, List<ComponentDescriptionDTO>> missingServices = new HashMap<String, List<ComponentDescriptionDTO>>();

        // find all components that fail to start due to missing service dependencies, group by missing service
        Iterator allComponentDescriptions = allComponents.iterator();
        while (allComponentDescriptions.hasNext()) {
            ComponentDescriptionDTO description = (ComponentDescriptionDTO) allComponentDescriptions.next();

            collectMissingServices(description, scr, serviceImpls, componentsByName, missingServices);
        }

        // sort by most blocked components first
        List<Map.Entry<String, List<ComponentDescriptionDTO>>> list = new ArrayList<Map.Entry<String, List<ComponentDescriptionDTO>>>(
                missingServices.entrySet());
        Collections.sort(list, new Comparator<Map.Entry<String, List<ComponentDescriptionDTO>>>() {
            public int compare(Map.Entry<String, List<ComponentDescriptionDTO>> o1,
                    Map.Entry<String, List<ComponentDescriptionDTO>> o2) {
                return o2.getValue().size() - o1.getValue().size();
            }
        });

        for (Map.Entry<String, List<ComponentDescriptionDTO>> entry : list) {
            List<ComponentDescriptionDTO> dependents = entry.getValue();
            // sort alphabetically by component name
            Collections.sort(dependents, new Comparator<ComponentDescriptionDTO>() {
                @Override
                public int compare(ComponentDescriptionDTO o1, ComponentDescriptionDTO o2) {
                    return o1.name.compareTo(o2.name);
                }
            });
            out.println("<div class='toggle'>");
            out.println("<div class='ui-icon ui-icon-triangle-1-e'></div>");
            out.print("missing service: ");
            out.print(entry.getKey());
            out.print(" blocks ");
            out.print(dependents.size());
            out.println(" other components");
            out.println("<br>");
            out.println("<div class='toggle-content' style='display:none'>");
            for (ComponentDescriptionDTO dependent : dependents) {
                out.print("<p>");
                out.print(dependent.name);
                out.println("</p>");
            }
            out.println("</div>");
            out.println("</div>");
            out.println("<br>");
        }

        out.println("</div>");

        //        out.println("<h2>Origins</h2>");
    }

    private String getServiceStatusLine(Collection<ComponentDescriptionDTO> allComponents,
            ServiceReference<?>[] allServiceReferences) {
        final StringBuilder builder = new StringBuilder();
        builder.append("Component information: ");
        builder.append(allComponents.size());
        builder.append(" different components, ");
        long componentsWithActiveInstances = 0;
        long totalInstances = 0;
        long factories = 0;
        long totalFactoryInstances = 0;
        for (ComponentDescriptionDTO component : allComponents) {
            int count = scr.getComponentConfigurationDTOs(component).size();
            if (count > 0) {
                componentsWithActiveInstances += 1;
            }
            totalInstances += count;
            if (component.factory != null) {
                factories += 1;
                totalFactoryInstances += count;
            }
        }
        builder.append(componentsWithActiveInstances);
        builder.append(" active components, ");
        builder.append(totalInstances);
        builder.append(" active instances, ");
        builder.append(factories);
        builder.append(" factory components, ");
        // is always 0 ???
        //        builder.append(totalFactoryInstances);
        //        builder.append(" total factory instances, ");
        builder.append(allServiceReferences.length);
        builder.append(" service references");
        return builder.toString();
    }

    /** Returns a map from service interface name to all active component implementations that provide this interface */
    private Map<String, List<ComponentDescriptionDTO>> getAvailableServiceImplementations(
            Iterable<ComponentDescriptionDTO> allComponents) {
        Map<String, List<ComponentDescriptionDTO>> serviceImpls = new HashMap<String, List<ComponentDescriptionDTO>>();

        Iterator allComponentDescriptions = allComponents.iterator();
        while (allComponentDescriptions.hasNext()) {
            ComponentDescriptionDTO description = (ComponentDescriptionDTO) allComponentDescriptions.next();
            //            log.info(description.name);
            //            if (description.name.contains("SharedS3DataStore")) {
            //                log.info(">>>>>>>>>>>>>> name = {}", description.name);
            //                log.info(">>>>>>>>>>>>>> immediate = {}", description.immediate);
            //                log.info(">>>>>>>>>>>>>> implementationClass = {}", description.implementationClass);
            //                log.info(">>>>>>>>>>>>>> factory = {}", description.factory);
            //                log.info(">>>>>>>>>>>>>> serviceInterfaces = {}", description.serviceInterfaces);
            //                log.info(">>>>>>>>>>>>>> configurationPid = {}", description.configurationPid);
            //                log.info(">>>>>>>>>>>>>> configurationPolicy = {}", description.configurationPolicy);
            //            }
            for (String serviceInterface : description.serviceInterfaces) {
                addToMultiMap(serviceImpls, serviceInterface, description);
            }
        }
        return serviceImpls;
    }

    private Map<String, List<ComponentDescriptionDTO>> getAvailableComponentsByName(
            Iterable<ComponentDescriptionDTO> allComponents) {
        Map<String, List<ComponentDescriptionDTO>> serviceImpls = new HashMap<String, List<ComponentDescriptionDTO>>();

        Iterator allComponentDescriptions = allComponents.iterator();
        while (allComponentDescriptions.hasNext()) {
            ComponentDescriptionDTO description = (ComponentDescriptionDTO) allComponentDescriptions.next();
            addToMultiMap(serviceImpls, description.name, description);
        }
        return serviceImpls;
    }

    /** collect missing services for instances of this component description */
    private void collectMissingServices(ComponentDescriptionDTO description, ServiceComponentRuntime scr,
            Map<String, List<ComponentDescriptionDTO>> serviceImpls,
            Map<String, List<ComponentDescriptionDTO>> componentsByName,
            Map<String, List<ComponentDescriptionDTO>> missingServices) {
        Iterator components = scr.getComponentConfigurationDTOs(description).iterator();

        // first instance is enough
        if (components.hasNext()) {
            ComponentConfigurationDTO component = (ComponentConfigurationDTO) components.next();
            //            if (description.name.contains("SalesforceExportProcess")) {
            //                log.info("################ name = {}", description.name);
            //                log.info("################ state = {}", component.state);
            //            }

            for (int i = 0; i < component.description.references.length; i++) {
                ReferenceDTO reference = component.description.references[i];
                SatisfiedReferenceDTO satisfiedRef = getSatisfiedReferenceDTO(component, reference.name);
                if (satisfiedRef == null) {
                    String serviceInterface = reference.interfaceName;

                    List impls = serviceImpls.get(serviceInterface);
                    if (impls == null) {
                        List<ComponentDescriptionDTO> missingComponents = componentsByName.get(serviceInterface);
                        String problem = "no component instance active";
                        if (missingComponents == null || missingComponents.isEmpty()) {
                            // component not even defined (e.g. bundle missing)
                            problem = "no component definition in active bundles found";
                        } else {
                            ComponentDescriptionDTO missingComponentDesc = missingComponents.get(0);
                            if ("require".equals(missingComponentDesc.configurationPolicy)) {
                                problem = "missing required config";
                            }
                        }
                        addToMultiMap(missingServices, serviceInterface + " (" + problem + ")", description);
                    }
                }
            }
        }
    }

    private <K, V> void addToMultiMap(Map<K, List<V>> map, K key, V value) {
        List<V> components = map.get(key);
        if (components == null) {
            components = new ArrayList<V>();
            map.put(key, components);
        }
        components.add(value);
    }

    private SatisfiedReferenceDTO getSatisfiedReferenceDTO(final ComponentConfigurationDTO component,
            final String name) {
        for (int i = 0; i < component.satisfiedReferences.length; i++) {
            SatisfiedReferenceDTO ref = component.satisfiedReferences[i];
            if (ref.name.equals(name)) {
                return ref;
            }
        }
        return null;
    }

    // ----------------------------------------------< html helper >---------------------------------

    private void includeCSS(PrintWriter out, String path) {
        out.print("<link href='");
        out.print(path);
        out.println("' rel='stylesheet' type='text/css' />");
    }

    private void embedScript(PrintWriter out, String path) {
        out.println("<script type='text/javascript'>");
        out.print("// ");
        out.println(path);
        includeResource(out, path);
        out.println("</script>");
    }

    private void embedStyle(PrintWriter out, String path) {
        out.println("<style>");
        out.print("/* ");
        out.print(path);
        out.println(" */");
        includeResource(out, path);
        out.println("</style>");
    }

    private void includeResource(PrintWriter out, String path) {
        try {
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            URL url = getClass().getResource(path);
            if (url == null) {
                // not found`
                return;
            }
            InputStream ins = url.openConnection().getInputStream();
            LineIterator lineIterator = IOUtils.lineIterator(ins, "UTF-8");

            boolean startComment = true;
            while (lineIterator.hasNext()) {
                String line = lineIterator.nextLine();
                if (startComment) {
                    String trimmed = line.trim();
                    if (!trimmed.isEmpty() && !trimmed.startsWith("/**") && !trimmed.startsWith("*")) {
                        startComment = false;
                    }
                }
                if (!startComment) {
                    out.println(line);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}