Package org.apache.felix.deploymentadmin

Source Code of org.apache.felix.deploymentadmin.DeploymentAdminImpl

/*
* 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 org.apache.felix.deploymentadmin;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.JarInputStream;

import org.apache.felix.deploymentadmin.spi.CommitResourceCommand;
import org.apache.felix.deploymentadmin.spi.DeploymentSessionImpl;
import org.apache.felix.deploymentadmin.spi.DropAllBundlesCommand;
import org.apache.felix.deploymentadmin.spi.DropAllResourcesCommand;
import org.apache.felix.deploymentadmin.spi.DropBundleCommand;
import org.apache.felix.deploymentadmin.spi.DropResourceCommand;
import org.apache.felix.deploymentadmin.spi.GetStorageAreaCommand;
import org.apache.felix.deploymentadmin.spi.ProcessResourceCommand;
import org.apache.felix.deploymentadmin.spi.SnapshotCommand;
import org.apache.felix.deploymentadmin.spi.StartBundleCommand;
import org.apache.felix.deploymentadmin.spi.StartCustomizerCommand;
import org.apache.felix.deploymentadmin.spi.StopBundleCommand;
import org.apache.felix.deploymentadmin.spi.UpdateCommand;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Version;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.osgi.service.deploymentadmin.DeploymentAdmin;
import org.osgi.service.deploymentadmin.DeploymentException;
import org.osgi.service.deploymentadmin.DeploymentPackage;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.log.LogService;
import org.osgi.service.packageadmin.PackageAdmin;

public class DeploymentAdminImpl implements DeploymentAdmin, ManagedService, Constants {
    /** Configuration PID used to dynamically configure DA at runtime. */
    public static final String PID = "org.apache.felix.deploymentadmin";

    public static final String PACKAGE_DIR = "packages";
    public static final String TEMP_DIR = "temp";
    public static final String PACKAGECONTENTS_DIR = "contents";
    public static final String PACKAGEINDEX_FILE = "index.txt";
    public static final String TEMP_PREFIX = "pkg";
    public static final String TEMP_POSTFIX = "";

    private static final long TIMEOUT = 10000;

    private volatile BundleContext m_context; /* will be injected by dependencymanager */
    private volatile PackageAdmin m_packageAdmin; /* will be injected by dependencymanager */
    private volatile EventAdmin m_eventAdmin; /* will be injected by dependencymanager */
    private volatile LogService m_log; /* will be injected by dependencymanager */
    private volatile DeploymentSessionImpl m_session;
    private volatile DeploymentAdminConfig m_config;

    private final Map /* BSN -> DeploymentPackage */m_packages = new HashMap();
    private final Semaphore m_semaphore = new Semaphore();

    /**
     * Creates a new {@link DeploymentAdminImpl} instance.
     */
    public DeploymentAdminImpl() {
        // Nop
    }

    /**
     * Creates a new {@link DeploymentAdminImpl} instance.
     */
    DeploymentAdminImpl(BundleContext context) {
        m_context = context;
    }

    public boolean cancel() {
        DeploymentSessionImpl session = m_session;
        if (session != null) {
            session.cancel();
            return true;
        }
        return false;
    }

    /**
     * Returns reference to this bundle's <code>BundleContext</code>
     *
     * @return This bundle's <code>BundleContext</code>
     */
    public BundleContext getBundleContext() {
        return m_context;
    }

    /**
     * @return the configuration for this {@link DeploymentAdmin} instance, never <code>null</code>.
     */
    public DeploymentAdminConfig getConfiguration() {
        return m_config;
    }

    public DeploymentPackage getDeploymentPackage(Bundle bundle) {
        if (bundle == null) {
            throw new IllegalArgumentException("Bundle can not be null");
        }
        return getDeploymentPackageContainingBundleWithSymbolicName(bundle.getSymbolicName());
    }

    public DeploymentPackage getDeploymentPackage(String symbName) {
        if (symbName == null) {
            throw new IllegalArgumentException("Symbolic name may not be null");
        }
        return (DeploymentPackage) m_packages.get(symbName);
    }

    /**
     * Returns reference to the current logging service defined in the framework.
     *
     * @return Currently active <code>LogService</code>.
     */
    public LogService getLog() {
        return m_log;
    }

    /**
     * Returns reference to the current package admin defined in the framework.
     *
     * @return Currently active <code>PackageAdmin</code>.
     */
    public PackageAdmin getPackageAdmin() {
        return m_packageAdmin;
    }

    public DeploymentPackage installDeploymentPackage(InputStream sourceInput) throws DeploymentException {
        if (sourceInput == null) {
            throw new IllegalArgumentException("Inputstream may not be null");
        }

        try {
            if (!m_semaphore.tryAcquire(TIMEOUT)) {
                throw new DeploymentException(CODE_TIMEOUT, "Timeout exceeded while waiting to install deployment package (" + TIMEOUT + " ms)");
            }
        }
        catch (InterruptedException ie) {
            throw new DeploymentException(CODE_TIMEOUT, "Thread interrupted");
        }

        File tempPackage = null;
        StreamDeploymentPackage source = null;
        AbstractDeploymentPackage target = null;
        boolean succeeded = false;

        try {
            JarInputStream jarInput = null;
            File tempIndex = null;
            File tempContents = null;
            try {
                File tempDir = m_context.getDataFile(TEMP_DIR);
                tempDir.mkdirs();
                tempPackage = File.createTempFile(TEMP_PREFIX, TEMP_POSTFIX, tempDir);
                tempPackage.delete();
                tempPackage.mkdirs();
                tempIndex = new File(tempPackage, PACKAGEINDEX_FILE);
                tempContents = new File(tempPackage, PACKAGECONTENTS_DIR);
                tempContents.mkdirs();
            }
            catch (IOException e) {
                m_log.log(LogService.LOG_ERROR, "Error writing package to disk", e);
                throw new DeploymentException(CODE_OTHER_ERROR, "Error writing package to disk", e);
            }

            try {
                jarInput = new ContentCopyingJarInputStream(sourceInput, tempIndex, tempContents);

                if (jarInput.getManifest() == null) {
                    m_log.log(LogService.LOG_ERROR, "Stream does not contain a valid deployment package: missing manifest!");
                    throw new DeploymentException(CODE_MISSING_HEADER, "No manifest present in deployment package!");
                }
            }
            catch (IOException e) {
                m_log.log(LogService.LOG_ERROR, "Stream does not contain a valid Jar", e);
                throw new DeploymentException(CODE_NOT_A_JAR, "Stream does not contain a valid Jar", e);
            }

            source = new StreamDeploymentPackage(jarInput, m_context, this);
            String dpSymbolicName = source.getName();

            target = getExistingOrEmptyDeploymentPackage(dpSymbolicName);

            // Fire an event that we're about to install a new package
            sendStartedEvent(source, target);

            // Assert that:
            // the source has no bundles that exists in other packages than the target.
            verifyNoResourcesShared(source, target);

            if (source.isFixPackage()) {
                // Assert that:
                // a. the version of the target matches the required fix-package range;
                // b. all missing source bundles are present in the target.
                verifyFixPackage(source, target);
            }
            else {
                // Assert that:
                // no missing resources or bundles are declared.
                verifySourcePackage(source);
            }

            try {
                m_session = new DeploymentSessionImpl(source, target, createInstallCommandChain(), this);
                m_session.call(false /* ignoreExceptions */);
            }
            catch (DeploymentException de) {
                throw de;
            }

            String dpInstallBaseDirectory = PACKAGE_DIR + File.separator + dpSymbolicName;

            File targetContents = m_context.getDataFile(dpInstallBaseDirectory + File.separator + PACKAGECONTENTS_DIR);
            File targetIndex = m_context.getDataFile(dpInstallBaseDirectory + File.separator + PACKAGEINDEX_FILE);

            if (source.isFixPackage()) {
                try {
                    Utils.merge(targetIndex, targetContents, tempIndex, tempContents);
                }
                catch (IOException e) {
                    m_log.log(LogService.LOG_ERROR, "Could not merge source fix package with target deployment package", e);
                    throw new DeploymentException(CODE_OTHER_ERROR, "Could not merge source fix package with target deployment package", e);
                }
            }
            else {
                File targetPackage = m_context.getDataFile(dpInstallBaseDirectory);
                targetPackage.mkdirs();
                if (!Utils.replace(targetPackage, tempPackage)) {
                    throw new DeploymentException(CODE_OTHER_ERROR, "Could not replace " + targetPackage + " with " + tempPackage);
                }
            }

            FileDeploymentPackage fileDeploymentPackage = null;
            try {
                fileDeploymentPackage = new FileDeploymentPackage(targetIndex, targetContents, m_context, this);
                m_packages.put(dpSymbolicName, fileDeploymentPackage);
            }
            catch (IOException e) {
                m_log.log(LogService.LOG_ERROR, "Could not create installed deployment package from disk", e);
                throw new DeploymentException(CODE_OTHER_ERROR, "Could not create installed deployment package from disk", e);
            }

            // Since we're here, it means everything went OK, so we might as well raise our success flag...
            succeeded = true;

            return fileDeploymentPackage;
        }
        finally {
            if (tempPackage != null) {
                if (!Utils.delete(tempPackage, true)) {
                    m_log.log(LogService.LOG_ERROR, "Could not delete temporary deployment package from disk");
                    succeeded = false;
                }
            }

            sendCompleteEvent(source, target, succeeded);
            m_semaphore.release();
        }
    }

    public DeploymentPackage[] listDeploymentPackages() {
        Collection packages = m_packages.values();
        return (DeploymentPackage[]) packages.toArray(new DeploymentPackage[packages.size()]);
    }

    /**
     * Called by dependency manager upon start of this component.
     */
    public void start() throws DeploymentException {
        // Create a default configuration...
        m_config = new DeploymentAdminConfig(m_context);

        File packageDir = m_context.getDataFile(PACKAGE_DIR);
        if (packageDir == null) {
            throw new DeploymentException(CODE_OTHER_ERROR, "Could not create directories needed for deployment package persistence");
        }
        else if (packageDir.isDirectory()) {
            File[] dpPackages = packageDir.listFiles();
            for (int i = 0; i < dpPackages.length; i++) {
                File dpPackageDir = dpPackages[i];
                if (!dpPackageDir.isDirectory()) {
                    continue;
                }

                try {
                    File index = new File(dpPackageDir, PACKAGEINDEX_FILE);
                    File contents = new File(dpPackageDir, PACKAGECONTENTS_DIR);
                    FileDeploymentPackage dp = new FileDeploymentPackage(index, contents, m_context, this);
                    m_packages.put(dp.getName(), dp);
                }
                catch (IOException e) {
                    m_log.log(LogService.LOG_WARNING, "Could not read deployment package from disk, skipping: '" + dpPackageDir.getAbsolutePath() + "'");
                }
            }
        }
    }

    /**
     * Called by dependency manager when stopping this component.
     */
    public void stop() {
        cancel();

        m_config = null;
    }

    /**
     * Uninstalls the given deployment package from the system.
     *
     * @param dp the deployment package to uninstall, cannot be <code>null</code>;
     * @param forced <code>true</code> to force the uninstall, meaning that any exceptions are ignored during the uninstallation.
     * @throws DeploymentException in case the uninstall failed.
     */
    public void uninstallDeploymentPackage(DeploymentPackage dp, boolean forced) throws DeploymentException {
        try {
            if (!m_semaphore.tryAcquire(TIMEOUT)) {
                throw new DeploymentException(CODE_TIMEOUT, "Timeout exceeded while waiting to uninstall deployment package (" + TIMEOUT + " ms)");
            }
        }
        catch (InterruptedException ie) {
            throw new DeploymentException(CODE_TIMEOUT, "Thread interrupted");
        }

        boolean succeeded = false;
        AbstractDeploymentPackage source = AbstractDeploymentPackage.EMPTY_PACKAGE;
        AbstractDeploymentPackage target = (AbstractDeploymentPackage) dp;

        // Notify listeners that we've about to uninstall the deployment package...
        sendUninstallEvent(source, target);

        try {
            try {
                m_session = new DeploymentSessionImpl(source, target, createUninstallCommandChain(), this);
                m_session.call(forced /* ignoreExceptions */);
            }
            catch (DeploymentException de) {
                throw de;
            }

            File targetPackage = m_context.getDataFile(PACKAGE_DIR + File.separator + source.getName());
            if (!Utils.delete(targetPackage, true)) {
                m_log.log(LogService.LOG_ERROR, "Could not delete deployment package from disk");
                throw new DeploymentException(CODE_OTHER_ERROR, "Could not delete deployment package from disk");
            }

            m_packages.remove(dp.getName());

            succeeded = true;
        }
        finally {
            sendCompleteEvent(source, target, succeeded);
            m_semaphore.release();
        }
    }

    public void updated(Dictionary properties) throws ConfigurationException {
        m_config = new DeploymentAdminConfig(m_context, properties);
    }

    /**
     * Creates the properties for a new event.
     *
     * @param source the source package being installed;
     * @param target the current installed package (can be new).
     * @return the event properties, never <code>null</code>.
     */
    private Dictionary createEventProperties(AbstractDeploymentPackage source, AbstractDeploymentPackage target) {
        Dictionary props = new Properties();
        if (source != null) {
            String displayName = source.getDisplayName();
            if (displayName == null) {
                displayName = source.getName();
            }

            props.put(EVENTPROPERTY_DEPLOYMENTPACKAGE_NAME, source.getName());
            props.put(EVENTPROPERTY_DEPLOYMENTPACKAGE_READABLENAME, displayName);
            if (!source.isNew()) {
                props.put(EVENTPROPERTY_DEPLOYMENTPACKAGE_NEXTVERSION, source.getVersion());
            }
        }
        if ((target != null) && !target.isNew()) {
            props.put(EVENTPROPERTY_DEPLOYMENTPACKAGE_CURRENTVERSION, target.getVersion());
        }
        return props;
    }

    private List createInstallCommandChain() {
        List commandChain = new ArrayList();

        GetStorageAreaCommand getStorageAreaCommand = new GetStorageAreaCommand();
        commandChain.add(getStorageAreaCommand);
        commandChain.add(new StopBundleCommand());
        commandChain.add(new SnapshotCommand(getStorageAreaCommand));
        commandChain.add(new UpdateCommand());
        commandChain.add(new StartCustomizerCommand());
        CommitResourceCommand commitCommand = new CommitResourceCommand();
        commandChain.add(new ProcessResourceCommand(commitCommand));
        commandChain.add(new DropResourceCommand(commitCommand));
        commandChain.add(new DropBundleCommand());
        commandChain.add(commitCommand);
        commandChain.add(new StartBundleCommand());

        return commandChain;
    }

    private List createUninstallCommandChain() {
        List commandChain = new ArrayList();

        GetStorageAreaCommand getStorageAreaCommand = new GetStorageAreaCommand();
        commandChain.add(getStorageAreaCommand);
        commandChain.add(new StopBundleCommand());
        commandChain.add(new SnapshotCommand(getStorageAreaCommand));
        commandChain.add(new StartCustomizerCommand());
        CommitResourceCommand commitCommand = new CommitResourceCommand();
        commandChain.add(new DropAllResourcesCommand(commitCommand));
        commandChain.add(commitCommand);
        commandChain.add(new DropAllBundlesCommand());

        return commandChain;
    }

    /**
     * Searches for a deployment package that contains a bundle with the given symbolic name.
     *
     * @param symbolicName the symbolic name of the <em>bundle</em> to return the containing deployment package for, cannot be <code>null</code>.
     * @return the deployment package containing the given bundle, or <code>null</code> if no deployment package contained such bundle.
     */
    private AbstractDeploymentPackage getDeploymentPackageContainingBundleWithSymbolicName(String symbolicName) {
        for (Iterator i = m_packages.values().iterator(); i.hasNext();) {
            AbstractDeploymentPackage dp = (AbstractDeploymentPackage) i.next();
            if (dp.getBundle(symbolicName) != null) {
                return dp;
            }
        }
        return null;
    }

    /**
     * Returns either an existing deployment package, or if no such package exists, an empty package.
     *
     * @param symbolicName the name of the deployment package to retrieve, cannot be <code>null</code>.
     * @return a deployment package, never <code>null</code>.
     */
    private AbstractDeploymentPackage getExistingOrEmptyDeploymentPackage(String symbolicName) {
        AbstractDeploymentPackage result = (AbstractDeploymentPackage) m_packages.get(symbolicName);
        if (result == null) {
            result = AbstractDeploymentPackage.EMPTY_PACKAGE;
        }
        return result;
    }

    /**
     * Returns all bundles that are not present in any deployment package. Ultimately, this should only
     * be one bundle, the system bundle, but this is not enforced in any way by the specification.
     *
     * @return an array of non-deployment packaged bundles, never <code>null</code>.
     */
    private Bundle[] getNonDeploymentPackagedBundles() {
        List result = new ArrayList(Arrays.asList(m_context.getBundles()));

        Iterator iter = result.iterator();
        while (iter.hasNext()) {
            Bundle suspect = (Bundle) iter.next();
            if (suspect.getLocation().startsWith(BUNDLE_LOCATION_PREFIX)) {
                iter.remove();
            }
        }

        return (Bundle[]) result.toArray(new Bundle[result.size()]);
    }

    /**
     * Sends out an event that the {@link #installDeploymentPackage(InputStream)} is
     * completed its installation of a deployment package.
     *
     * @param source the source package being installed;
     * @param target the current installed package (can be new);
     * @param success <code>true</code> if the installation was successful, <code>false</code> otherwise.
     */
    private void sendCompleteEvent(AbstractDeploymentPackage source, AbstractDeploymentPackage target, boolean success) {
        Dictionary props = createEventProperties(source, target);
        props.put(EVENTPROPERTY_SUCCESSFUL, Boolean.valueOf(success));

        m_eventAdmin.postEvent(new Event(EVENTTOPIC_COMPLETE, props));
    }

    /**
     * Sends out an event that the {@link #installDeploymentPackage(InputStream)} is about
     * to install a new deployment package.
     *
     * @param source the source package being installed;
     * @param target the current installed package (can be new).
     */
    private void sendStartedEvent(AbstractDeploymentPackage source, AbstractDeploymentPackage target) {
        Dictionary props = createEventProperties(source, target);

        m_eventAdmin.postEvent(new Event(EVENTTOPIC_INSTALL, props));
    }

    /**
     * Sends out an event that the {@link #uninstallDeploymentPackage(DeploymentPackage)} is about
     * to uninstall a deployment package.
     *
     * @param source the source package being uninstalled;
     * @param target the current installed package (can be new).
     */
    private void sendUninstallEvent(AbstractDeploymentPackage source, AbstractDeploymentPackage target) {
        Dictionary props = createEventProperties(source, target);

        m_eventAdmin.postEvent(new Event(EVENTTOPIC_UNINSTALL, props));
    }

    /**
     * Verifies that the version of the target matches the required source version range, and
     * whether all missing source resources are available in the target.
     *
     * @param source the fix-package source to verify;
     * @param target the target package to verify against.
     * @throws DeploymentException in case verification failed.
     */
    private void verifyFixPackage(AbstractDeploymentPackage source, AbstractDeploymentPackage target) throws DeploymentException {
        boolean newPackage = target.isNew();

        // Verify whether the target package exists, and if so, falls in the requested fix-package range...
        if (newPackage || (!source.getVersionRange().isInRange(target.getVersion()))) {
            m_log.log(LogService.LOG_ERROR, "Target package version '" + target.getVersion() + "' is not in source range '" + source.getVersionRange() + "'");
            throw new DeploymentException(CODE_MISSING_FIXPACK_TARGET, "Target package version '" + target.getVersion() + "' is not in source range '" + source.getVersionRange() + "'");
        }

        // Verify whether all missing bundles are available in the target package...
        BundleInfoImpl[] bundleInfos = source.getBundleInfoImpls();
        for (int i = 0; i < bundleInfos.length; i++) {
            if (bundleInfos[i].isMissing()) {
                // Check whether the bundle exists in the target package...
                BundleInfoImpl targetBundleInfo = target.getBundleInfoByPath(bundleInfos[i].getPath());
                if (targetBundleInfo == null) {
                    m_log.log(LogService.LOG_ERROR, "Missing bundle '" + bundleInfos[i].getSymbolicName() + "/" + bundleInfos[i].getVersion() + " does not exist in target package!");
                    throw new DeploymentException(CODE_MISSING_BUNDLE, "Missing bundle '" + bundleInfos[i].getSymbolicName() + "/" + bundleInfos[i].getVersion() + " does not exist in target package!");
                }
            }
        }

        // Verify whether all missing resources are available in the target package...
        ResourceInfoImpl[] resourceInfos = source.getResourceInfos();
        for (int i = 0; i < resourceInfos.length; i++) {
            if (resourceInfos[i].isMissing()) {
                // Check whether the resource exists in the target package...
                ResourceInfoImpl targetResourceInfo = target.getResourceInfoByPath(resourceInfos[i].getPath());
                if (targetResourceInfo == null) {
                    m_log.log(LogService.LOG_ERROR, "Missing resource '" + resourceInfos[i].getPath() + " does not exist in target package!");
                    throw new DeploymentException(CODE_MISSING_RESOURCE, "Missing resource '" + resourceInfos[i].getPath() + " does not exist in target package!");
                }
            }
        }
    }

    /**
     * Verifies whether none of the mentioned resources in the source package are present in
     * deployment packages other than the given target.
     *
     * @param source the source package to verify;
     * @param target the target package to verify against.
     * @throws DeploymentException in case verification fails.
     */
    private void verifyNoResourcesShared(AbstractDeploymentPackage source, AbstractDeploymentPackage target) throws DeploymentException {
        Bundle[] foreignBundles = getNonDeploymentPackagedBundles();

        // Verify whether all source bundles are available in the target package or absent...
        BundleInfoImpl[] bundleInfos = source.getBundleInfoImpls();
        for (int i = 0; i < bundleInfos.length; i++) {
            String symbolicName = bundleInfos[i].getSymbolicName();
            Version version = bundleInfos[i].getVersion();

            DeploymentPackage targetPackage = getDeploymentPackageContainingBundleWithSymbolicName(symbolicName);
            // If found, it should match the given target DP; not found is also ok...
            if ((targetPackage != null) && !targetPackage.equals(target)) {
                m_log.log(LogService.LOG_ERROR, "Bundle '" + symbolicName + "/" + version + " already present in other deployment packages!");
                throw new DeploymentException(CODE_BUNDLE_SHARING_VIOLATION, "Bundle '" + symbolicName + "/" + version + " already present in other deployment packages!");
            }

            if (targetPackage == null) {
                // Maybe the bundle is installed without deployment admin...
                for (int j = 0; j < foreignBundles.length; j++) {
                    if (symbolicName.equals(foreignBundles[j].getSymbolicName()) && version.equals(foreignBundles[j].getVersion())) {
                        m_log.log(LogService.LOG_ERROR, "Bundle '" + symbolicName + "/" + version + " already present!");
                        throw new DeploymentException(CODE_BUNDLE_SHARING_VIOLATION, "Bundle '" + symbolicName + "/" + version + " already present!");
                    }
                }
            }
        }

        // TODO verify other resources as well...
    }

    private void verifySourcePackage(AbstractDeploymentPackage source) throws DeploymentException {
        // TODO this method should do a X-ref check between DP-manifest and JAR-entries...
// m_log.log(LogService.LOG_ERROR, "Missing bundle '" + symbolicName + "/" + bundleInfos[i].getVersion() + " does not exist in target package!");
// throw new DeploymentException(CODE_OTHER_ERROR, "Missing bundle '" + symbolicName + "/" + bundleInfos[i].getVersion() + " is not part of target package!");
    }
}
TOP

Related Classes of org.apache.felix.deploymentadmin.DeploymentAdminImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.